VYPR
Medium severity5.4NVD Advisory· Published Apr 10, 2026· Updated Apr 13, 2026

CVE-2026-35620

CVE-2026-35620

Description

OpenClaw before 2026.3.24 contains missing authorization vulnerabilities in the /send and /allowlist chat command handlers. The /send command allows non-owner command-authorized senders to change owner-only session delivery policy settings, and the /allowlist mutating commands fail to enforce operator.admin scope. Attackers with operator.write scope can invoke /send on|off|inherit to persistently mutate the current session's sendPolicy, and execute /allowlist add commands to modify config-backed allowFrom entries and pairing-store allowlist entries without proper admin authorization.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.242026.3.24

Affected products

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.3.24

Patches

3
ccfeecb6887c

test: harden parallels macos dashboard smoke

https://github.com/openclaw/openclawPeter SteinbergerMar 23, 2026via ghsa
2 files changed · +107 3
  • .agents/skills/openclaw-parallels-smoke/SKILL.md+1 0 modified
    @@ -32,6 +32,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
     - Preferred entrypoint: `pnpm test:parallels:macos`
     - Default to the snapshot closest to `macOS 26.3.1 latest`.
     - On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.3.1 latest'` before blaming auth or the harness.
    +- The macOS smoke should include a dashboard load phase after gateway health: resolve the tokenized URL with `openclaw dashboard --no-open`, verify the served HTML contains the Control UI title/root shell, then open Safari and require an established localhost TCP connection from Safari to the gateway port.
     - `prlctl exec` is fine for deterministic repo commands, but use the guest Terminal or `prlctl enter` when installer parity or shell-sensitive behavior matters.
     - Multi-word `openclaw agent --message ...` checks should go through a guest shell wrapper (`guest_current_user_sh` / `guest_current_user_cli` or `/bin/sh -lc ...`), not raw `prlctl exec ... node openclaw.mjs ...`, or the message can be split into extra argv tokens and Commander reports `too many arguments for 'agent'`.
     - On the fresh Tahoe snapshot, `brew` exists but `node` may be missing from PATH in noninteractive exec. Use `/opt/homebrew/bin/node` when needed.
    
  • scripts/e2e/parallels-macos-smoke.sh+106 3 modified
    @@ -31,6 +31,7 @@ GUEST_NPM_BIN="/opt/homebrew/bin/npm"
     
     MAIN_TGZ_DIR="$(mktemp -d)"
     MAIN_TGZ_PATH=""
    +PACKED_MAIN_COMMIT_SHORT=""
     SERVER_PID=""
     RUN_DIR="$(mktemp -d /tmp/openclaw-parallels-smoke.XXXXXX)"
     BUILD_LOCK_DIR="${TMPDIR:-/tmp}/openclaw-parallels-build.lock"
    @@ -41,6 +42,7 @@ TIMEOUT_ONBOARD_S=180
     TIMEOUT_GATEWAY_S=60
     TIMEOUT_AGENT_S=120
     TIMEOUT_PERMISSION_S=60
    +TIMEOUT_DASHBOARD_S=60
     TIMEOUT_SNAPSHOT_S=180
     TIMEOUT_DISCORD_S=180
     
    @@ -51,6 +53,8 @@ FRESH_GATEWAY_STATUS="skip"
     UPGRADE_GATEWAY_STATUS="skip"
     FRESH_AGENT_STATUS="skip"
     UPGRADE_AGENT_STATUS="skip"
    +FRESH_DASHBOARD_STATUS="skip"
    +UPGRADE_DASHBOARD_STATUS="skip"
     FRESH_DISCORD_STATUS="skip"
     UPGRADE_DISCORD_STATUS="skip"
     
    @@ -562,8 +566,12 @@ extract_package_version_from_tgz() {
       tar -xOf "$1" package/package.json | python3 -c 'import json, sys; print(json.load(sys.stdin)["version"])'
     }
     
    +extract_package_build_commit_from_tgz() {
    +  tar -xOf "$1" package/dist/build-info.json | python3 -c 'import json, sys; print(json.load(sys.stdin).get("commit", ""))'
    +}
    +
     pack_main_tgz() {
    -  local short_head pkg
    +  local short_head pkg packed_commit
       if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then
         say "Pack target package tgz: $TARGET_PACKAGE_SPEC"
         pkg="$(
    @@ -578,13 +586,17 @@ pack_main_tgz() {
       fi
       say "Pack current main tgz"
       ensure_current_build
    +  stage_pack_runtime_deps
       short_head="$(git rev-parse --short HEAD)"
       pkg="$(
         npm pack --ignore-scripts --json --pack-destination "$MAIN_TGZ_DIR" \
           | python3 -c 'import json, sys; data = json.load(sys.stdin); print(data[-1]["filename"])'
       )"
       MAIN_TGZ_PATH="$MAIN_TGZ_DIR/openclaw-main-$short_head.tgz"
       cp "$MAIN_TGZ_DIR/$pkg" "$MAIN_TGZ_PATH"
    +  packed_commit="$(extract_package_build_commit_from_tgz "$MAIN_TGZ_PATH")"
    +  [[ -n "$packed_commit" ]] || die "failed to read packed build commit from $MAIN_TGZ_PATH"
    +  PACKED_MAIN_COMMIT_SHORT="${packed_commit:0:7}"
       say "Packed $MAIN_TGZ_PATH"
       tar -xOf "$MAIN_TGZ_PATH" package/dist/build-info.json
     }
    @@ -594,7 +606,8 @@ verify_target_version() {
         verify_version_contains "$TARGET_EXPECT_VERSION"
         return
       fi
    -  verify_version_contains "$(git rev-parse --short=7 HEAD)"
    +  [[ -n "$PACKED_MAIN_COMMIT_SHORT" ]] || die "packed main commit not captured"
    +  verify_version_contains "$PACKED_MAIN_COMMIT_SHORT"
     }
     
     current_build_commit() {
    @@ -610,6 +623,10 @@ else:
     PY
     }
     
    +current_control_ui_ready() {
    +  [[ -f "dist/control-ui/index.html" ]]
    +}
    +
     acquire_build_lock() {
       local owner_pid=""
       while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do
    @@ -637,15 +654,22 @@ ensure_current_build() {
       acquire_build_lock
       head="$(git rev-parse HEAD)"
       build_commit="$(current_build_commit)"
    -  if [[ "$build_commit" == "$head" ]]; then
    +  if [[ "$build_commit" == "$head" ]] && current_control_ui_ready; then
         release_build_lock
         return
       fi
       say "Build dist for current head"
       pnpm build
    +  say "Build Control UI for current head"
    +  pnpm ui:build
       build_commit="$(current_build_commit)"
       release_build_lock
       [[ "$build_commit" == "$head" ]] || die "dist/build-info.json still does not match HEAD after build"
    +  current_control_ui_ready || die "dist/control-ui/index.html missing after ui build"
    +}
    +
    +stage_pack_runtime_deps() {
    +  node scripts/stage-bundled-plugin-runtime-deps.mjs
     }
     
     start_server() {
    @@ -719,6 +743,77 @@ verify_turn() {
         --json
     }
     
    +resolve_dashboard_url() {
    +  local dashboard_url
    +  dashboard_url="$(
    +    guest_current_user_cli "$GUEST_OPENCLAW_BIN" dashboard --no-open \
    +      | awk '/^Dashboard URL: / { sub(/^Dashboard URL: /, ""); print; exit }'
    +  )"
    +  dashboard_url="${dashboard_url//$'\r'/}"
    +  dashboard_url="${dashboard_url//$'\n'/}"
    +  [[ -n "$dashboard_url" ]] || {
    +    echo "failed to resolve dashboard URL from openclaw dashboard --no-open" >&2
    +    return 1
    +  }
    +  printf '%s\n' "$dashboard_url"
    +}
    +
    +verify_dashboard_load() {
    +  local dashboard_url dashboard_http_url dashboard_url_q dashboard_http_url_q cmd
    +  dashboard_url="$(resolve_dashboard_url)"
    +  dashboard_http_url="${dashboard_url%%#*}"
    +  dashboard_url_q="$(shell_quote "$dashboard_url")"
    +  dashboard_http_url_q="$(shell_quote "$dashboard_http_url")"
    +  cmd="$(cat <<EOF
    +set -eu
    +export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin:\${PATH:-}"
    +if [ -z "\${HOME:-}" ]; then export HOME="/Users/\$(id -un)"; fi
    +cd "\$HOME"
    +dashboard_url=$dashboard_url_q
    +dashboard_http_url=$dashboard_http_url_q
    +dashboard_port=\$(printf '%s\n' "\$dashboard_http_url" | sed -E 's#^https?://[^:/]+:([0-9]+).*\$#\1#')
    +if [ -z "\$dashboard_port" ] || [ "\$dashboard_port" = "\$dashboard_http_url" ]; then
    +  echo "failed to parse dashboard port from \$dashboard_http_url" >&2
    +  exit 1
    +fi
    +deadline=\$((SECONDS + 30))
    +dashboard_ready=0
    +while [ \$SECONDS -lt \$deadline ]; do
    +  if curl -fsSL "\$dashboard_http_url" >/tmp/openclaw-dashboard-smoke.html 2>/dev/null; then
    +    if grep -F '<title>OpenClaw Control</title>' /tmp/openclaw-dashboard-smoke.html >/dev/null; then
    +      if grep -F '<openclaw-app></openclaw-app>' /tmp/openclaw-dashboard-smoke.html >/dev/null; then
    +        dashboard_ready=1
    +        break
    +      fi
    +    fi
    +  fi
    +  sleep 1
    +done
    +[ "\$dashboard_ready" = "1" ] || {
    +  echo "dashboard HTML did not become ready at \$dashboard_http_url" >&2
    +  exit 1
    +}
    +grep -F '<title>OpenClaw Control</title>' /tmp/openclaw-dashboard-smoke.html >/dev/null
    +grep -F '<openclaw-app></openclaw-app>' /tmp/openclaw-dashboard-smoke.html >/dev/null
    +pkill -x Safari >/dev/null 2>&1 || true
    +open -a Safari "\$dashboard_url"
    +deadline=\$((SECONDS + 20))
    +while [ \$SECONDS -lt \$deadline ]; do
    +  if pgrep -x Safari >/dev/null 2>&1; then
    +    if lsof -nPiTCP:"\$dashboard_port" -sTCP:ESTABLISHED 2>/dev/null \
    +      | awk 'NR > 1 && \$1 != "node" { found = 1 } END { exit found ? 0 : 1 }'; then
    +      exit 0
    +    fi
    +  fi
    +  sleep 1
    +done
    +echo "Safari did not establish a dashboard client connection on port \$dashboard_port" >&2
    +exit 1
    +EOF
    +)"
    +  guest_current_user_exec /bin/sh -lc "$cmd"
    +}
    +
     configure_discord_smoke() {
       local guilds_json script
       guilds_json="$(
    @@ -996,6 +1091,7 @@ summary = {
             "version": os.environ["SUMMARY_FRESH_MAIN_VERSION"],
             "gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"],
             "agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"],
    +        "dashboard": os.environ["SUMMARY_FRESH_DASHBOARD_STATUS"],
             "discord": os.environ["SUMMARY_FRESH_DISCORD_STATUS"],
         },
         "upgrade": {
    @@ -1005,6 +1101,7 @@ summary = {
             "mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
             "gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"],
             "agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"],
    +        "dashboard": os.environ["SUMMARY_UPGRADE_DASHBOARD_STATUS"],
             "discord": os.environ["SUMMARY_UPGRADE_DISCORD_STATUS"],
         },
     }
    @@ -1041,6 +1138,8 @@ run_fresh_main_lane() {
       phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
       phase_run "fresh.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway
       FRESH_GATEWAY_STATUS="pass"
    +  phase_run "fresh.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
    +  FRESH_DASHBOARD_STATUS="pass"
       phase_run "fresh.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn
       FRESH_AGENT_STATUS="pass"
       if discord_smoke_enabled; then
    @@ -1074,6 +1173,8 @@ run_upgrade_lane() {
       phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
       phase_run "upgrade.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway
       UPGRADE_GATEWAY_STATUS="pass"
    +  phase_run "upgrade.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
    +  UPGRADE_DASHBOARD_STATUS="pass"
       phase_run "upgrade.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn
       UPGRADE_AGENT_STATUS="pass"
       if discord_smoke_enabled; then
    @@ -1153,13 +1254,15 @@ SUMMARY_JSON_PATH="$(
       SUMMARY_FRESH_MAIN_VERSION="$FRESH_MAIN_VERSION" \
       SUMMARY_FRESH_GATEWAY_STATUS="$FRESH_GATEWAY_STATUS" \
       SUMMARY_FRESH_AGENT_STATUS="$FRESH_AGENT_STATUS" \
    +  SUMMARY_FRESH_DASHBOARD_STATUS="$FRESH_DASHBOARD_STATUS" \
       SUMMARY_FRESH_DISCORD_STATUS="$FRESH_DISCORD_STATUS" \
       SUMMARY_UPGRADE_PRECHECK_STATUS="$UPGRADE_PRECHECK_STATUS" \
       SUMMARY_UPGRADE_STATUS="$UPGRADE_STATUS" \
       SUMMARY_LATEST_INSTALLED_VERSION="$LATEST_INSTALLED_VERSION" \
       SUMMARY_UPGRADE_MAIN_VERSION="$UPGRADE_MAIN_VERSION" \
       SUMMARY_UPGRADE_GATEWAY_STATUS="$UPGRADE_GATEWAY_STATUS" \
       SUMMARY_UPGRADE_AGENT_STATUS="$UPGRADE_AGENT_STATUS" \
    +  SUMMARY_UPGRADE_DASHBOARD_STATUS="$UPGRADE_DASHBOARD_STATUS" \
       SUMMARY_UPGRADE_DISCORD_STATUS="$UPGRADE_DISCORD_STATUS" \
       write_summary_json
     )"
    
555b2578a8cc

feat: add /allowlist command

https://github.com/openclaw/openclawPeter SteinbergerJan 21, 2026via ghsa
7 files changed · +839 0
  • CHANGELOG.md+1 0 modified
    @@ -13,6 +13,7 @@ Docs: https://docs.clawd.bot
     - Memory: add native Gemini embeddings provider for memory search. (#1151) https://docs.clawd.bot/concepts/memory
     
     ### Changes
    +- Commands: add `/allowlist` slash command for listing and editing channel allowlists.
     - Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.clawd.bot/web/control-ui
     - Control UI: drop the legacy list view. (#1345) https://docs.clawd.bot/web/control-ui
     - TUI: add syntax highlighting for code blocks. (#1200) https://docs.clawd.bot/tui
    
  • docs/tools/slash-commands.md+2 0 modified
    @@ -60,6 +60,7 @@ Text + native (when enabled):
     - `/commands`
     - `/skill <name> [input]` (run a skill by name)
     - `/status` (show current status; includes provider usage/quota for the current model provider when available)
    +- `/allowlist` (list/add/remove allowlist entries)
     - `/context [list|detail|json]` (explain “context”; `detail` shows per-file + per-tool + per-skill + system prompt size)
     - `/whoami` (show your sender id; alias: `/id`)
     - `/subagents list|stop|log|info|send` (inspect, stop, log, or message sub-agent runs for the current session)
    @@ -93,6 +94,7 @@ Notes:
     - Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
     - `/new <model>` accepts a model alias, `provider/model`, or a provider name (fuzzy match); if no match, the text is treated as the message body.
     - For full provider usage breakdown, use `clawdbot status --usage`.
    +- `/allowlist add|remove` requires `commands.config=true` and honors channel `configWrites`.
     - `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from Clawdbot session logs.
     - `/restart` is disabled by default; set `commands.restart: true` to enable it.
     - `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
    
  • src/auto-reply/commands-registry.data.ts+7 0 modified
    @@ -157,6 +157,13 @@ function buildChatCommands(): ChatCommandDefinition[] {
           description: "Show current status.",
           textAlias: "/status",
         }),
    +    defineChatCommand({
    +      key: "allowlist",
    +      description: "List/add/remove allowlist entries.",
    +      textAlias: "/allowlist",
    +      acceptsArgs: true,
    +      scope: "text",
    +    }),
         defineChatCommand({
           key: "context",
           nativeName: "context",
    
  • src/auto-reply/reply/commands-allowlist.test.ts+139 0 added
    @@ -0,0 +1,139 @@
    +import { describe, expect, it, vi } from "vitest";
    +
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import type { MsgContext } from "../templating.js";
    +import { buildCommandContext, handleCommands } from "./commands.js";
    +import { parseInlineDirectives } from "./directive-handling.js";
    +
    +const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
    +const validateConfigObjectWithPluginsMock = vi.hoisted(() => vi.fn());
    +const writeConfigFileMock = vi.hoisted(() => vi.fn());
    +
    +vi.mock("../../config/config.js", async () => {
    +  const actual =
    +    await vi.importActual<typeof import("../../config/config.js")>("../../config/config.js");
    +  return {
    +    ...actual,
    +    readConfigFileSnapshot: readConfigFileSnapshotMock,
    +    validateConfigObjectWithPlugins: validateConfigObjectWithPluginsMock,
    +    writeConfigFile: writeConfigFileMock,
    +  };
    +});
    +
    +const readChannelAllowFromStoreMock = vi.hoisted(() => vi.fn());
    +const addChannelAllowFromStoreEntryMock = vi.hoisted(() => vi.fn());
    +const removeChannelAllowFromStoreEntryMock = vi.hoisted(() => vi.fn());
    +
    +vi.mock("../../pairing/pairing-store.js", async () => {
    +  const actual = await vi.importActual<typeof import("../../pairing/pairing-store.js")>(
    +    "../../pairing/pairing-store.js",
    +  );
    +  return {
    +    ...actual,
    +    readChannelAllowFromStore: readChannelAllowFromStoreMock,
    +    addChannelAllowFromStoreEntry: addChannelAllowFromStoreEntryMock,
    +    removeChannelAllowFromStoreEntry: removeChannelAllowFromStoreEntryMock,
    +  };
    +});
    +
    +vi.mock("../../channels/plugins/pairing.js", async () => {
    +  const actual = await vi.importActual<typeof import("../../channels/plugins/pairing.js")>(
    +    "../../channels/plugins/pairing.js",
    +  );
    +  return {
    +    ...actual,
    +    listPairingChannels: () => ["telegram"],
    +  };
    +});
    +
    +function buildParams(commandBody: string, cfg: ClawdbotConfig, ctxOverrides?: Partial<MsgContext>) {
    +  const ctx = {
    +    Body: commandBody,
    +    CommandBody: commandBody,
    +    CommandSource: "text",
    +    CommandAuthorized: true,
    +    Provider: "telegram",
    +    Surface: "telegram",
    +    ...ctxOverrides,
    +  } as MsgContext;
    +
    +  const command = buildCommandContext({
    +    ctx,
    +    cfg,
    +    isGroup: false,
    +    triggerBodyNormalized: commandBody.trim().toLowerCase(),
    +    commandAuthorized: true,
    +  });
    +
    +  return {
    +    ctx,
    +    cfg,
    +    command,
    +    directives: parseInlineDirectives(commandBody),
    +    elevated: { enabled: true, allowed: true, failures: [] },
    +    sessionKey: "agent:main:main",
    +    workspaceDir: "/tmp",
    +    defaultGroupActivation: () => "mention",
    +    resolvedVerboseLevel: "off" as const,
    +    resolvedReasoningLevel: "off" as const,
    +    resolveDefaultThinkingLevel: async () => undefined,
    +    provider: "telegram",
    +    model: "test-model",
    +    contextTokens: 0,
    +    isGroup: false,
    +  };
    +}
    +
    +describe("handleCommands /allowlist", () => {
    +  it("lists config + store allowFrom entries", async () => {
    +    readChannelAllowFromStoreMock.mockResolvedValueOnce(["456"]);
    +
    +    const cfg = {
    +      commands: { text: true },
    +      channels: { telegram: { allowFrom: ["123", "@Alice"] } },
    +    } as ClawdbotConfig;
    +    const params = buildParams("/allowlist list dm", cfg);
    +    const result = await handleCommands(params);
    +
    +    expect(result.shouldContinue).toBe(false);
    +    expect(result.reply?.text).toContain("Channel: telegram");
    +    expect(result.reply?.text).toContain("DM allowFrom (config): 123, @alice");
    +    expect(result.reply?.text).toContain("Paired allowFrom (store): 456");
    +  });
    +
    +  it("adds entries to config and pairing store", async () => {
    +    readConfigFileSnapshotMock.mockResolvedValueOnce({
    +      valid: true,
    +      parsed: {
    +        channels: { telegram: { allowFrom: ["123"] } },
    +      },
    +    });
    +    validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
    +      ok: true,
    +      config,
    +    }));
    +    addChannelAllowFromStoreEntryMock.mockResolvedValueOnce({
    +      changed: true,
    +      allowFrom: ["123", "789"],
    +    });
    +
    +    const cfg = {
    +      commands: { text: true, config: true },
    +      channels: { telegram: { allowFrom: ["123"] } },
    +    } as ClawdbotConfig;
    +    const params = buildParams("/allowlist add dm 789", cfg);
    +    const result = await handleCommands(params);
    +
    +    expect(result.shouldContinue).toBe(false);
    +    expect(writeConfigFileMock).toHaveBeenCalledWith(
    +      expect.objectContaining({
    +        channels: { telegram: { allowFrom: ["123", "789"] } },
    +      }),
    +    );
    +    expect(addChannelAllowFromStoreEntryMock).toHaveBeenCalledWith({
    +      channel: "telegram",
    +      entry: "789",
    +    });
    +    expect(result.reply?.text).toContain("DM allowlist added");
    +  });
    +});
    
  • src/auto-reply/reply/commands-allowlist.ts+657 0 added
    @@ -0,0 +1,657 @@
    +import {
    +  readConfigFileSnapshot,
    +  validateConfigObjectWithPlugins,
    +  writeConfigFile,
    +} from "../../config/config.js";
    +import { resolveChannelConfigWrites } from "../../channels/plugins/config-writes.js";
    +import { getChannelDock } from "../../channels/dock.js";
    +import { normalizeChannelId } from "../../channels/registry.js";
    +import { listPairingChannels } from "../../channels/plugins/pairing.js";
    +import { logVerbose } from "../../globals.js";
    +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
    +import { resolveDiscordAccount } from "../../discord/accounts.js";
    +import { resolveIMessageAccount } from "../../imessage/accounts.js";
    +import { resolveSignalAccount } from "../../signal/accounts.js";
    +import { resolveSlackAccount } from "../../slack/accounts.js";
    +import { resolveTelegramAccount } from "../../telegram/accounts.js";
    +import { resolveWhatsAppAccount } from "../../web/accounts.js";
    +import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
    +import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js";
    +import {
    +  addChannelAllowFromStoreEntry,
    +  readChannelAllowFromStore,
    +  removeChannelAllowFromStoreEntry,
    +} from "../../pairing/pairing-store.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import type { ChannelId } from "../../channels/plugins/types.js";
    +import type { CommandHandler } from "./commands-types.js";
    +
    +type AllowlistScope = "dm" | "group" | "all";
    +type AllowlistAction = "list" | "add" | "remove";
    +type AllowlistTarget = "both" | "config" | "store";
    +
    +type AllowlistCommand =
    +  | {
    +      action: "list";
    +      scope: AllowlistScope;
    +      channel?: string;
    +      account?: string;
    +      resolve?: boolean;
    +    }
    +  | {
    +      action: "add" | "remove";
    +      scope: AllowlistScope;
    +      channel?: string;
    +      account?: string;
    +      entry: string;
    +      resolve?: boolean;
    +      target: AllowlistTarget;
    +    }
    +  | { action: "error"; message: string };
    +
    +const ACTIONS = new Set(["list", "add", "remove"]);
    +const SCOPES = new Set<AllowlistScope>(["dm", "group", "all"]);
    +
    +function parseAllowlistCommand(raw: string): AllowlistCommand | null {
    +  const trimmed = raw.trim();
    +  if (!trimmed.toLowerCase().startsWith("/allowlist")) return null;
    +  const rest = trimmed.slice("/allowlist".length).trim();
    +  if (!rest) return { action: "list", scope: "dm" };
    +
    +  const tokens = rest.split(/\s+/);
    +  let action: AllowlistAction = "list";
    +  let scope: AllowlistScope = "dm";
    +  let resolve = false;
    +  let target: AllowlistTarget = "both";
    +  let channel: string | undefined;
    +  let account: string | undefined;
    +  const entryTokens: string[] = [];
    +
    +  let i = 0;
    +  if (tokens[i] && ACTIONS.has(tokens[i].toLowerCase())) {
    +    action = tokens[i].toLowerCase() as AllowlistAction;
    +    i += 1;
    +  }
    +  if (tokens[i] && SCOPES.has(tokens[i].toLowerCase() as AllowlistScope)) {
    +    scope = tokens[i].toLowerCase() as AllowlistScope;
    +    i += 1;
    +  }
    +
    +  for (; i < tokens.length; i += 1) {
    +    const token = tokens[i];
    +    const lowered = token.toLowerCase();
    +    if (lowered === "--resolve" || lowered === "resolve") {
    +      resolve = true;
    +      continue;
    +    }
    +    if (lowered === "--config" || lowered === "config") {
    +      target = "config";
    +      continue;
    +    }
    +    if (lowered === "--store" || lowered === "store") {
    +      target = "store";
    +      continue;
    +    }
    +    if (lowered === "--channel" && tokens[i + 1]) {
    +      channel = tokens[i + 1];
    +      i += 1;
    +      continue;
    +    }
    +    if (lowered === "--account" && tokens[i + 1]) {
    +      account = tokens[i + 1];
    +      i += 1;
    +      continue;
    +    }
    +    const kv = token.split("=");
    +    if (kv.length === 2) {
    +      const key = kv[0]?.trim().toLowerCase();
    +      const value = kv[1]?.trim();
    +      if (key === "channel") {
    +        if (value) channel = value;
    +        continue;
    +      }
    +      if (key === "account") {
    +        if (value) account = value;
    +        continue;
    +      }
    +      if (key === "scope" && value && SCOPES.has(value.toLowerCase() as AllowlistScope)) {
    +        scope = value.toLowerCase() as AllowlistScope;
    +        continue;
    +      }
    +    }
    +    entryTokens.push(token);
    +  }
    +
    +  if (action === "add" || action === "remove") {
    +    const entry = entryTokens.join(" ").trim();
    +    if (!entry) {
    +      return { action: "error", message: "Usage: /allowlist add|remove <entry>" };
    +    }
    +    return { action, scope, entry, channel, account, resolve, target };
    +  }
    +
    +  return { action: "list", scope, channel, account, resolve };
    +}
    +
    +function normalizeAllowFrom(params: {
    +  cfg: ClawdbotConfig;
    +  channelId: ChannelId;
    +  accountId?: string | null;
    +  values: Array<string | number>;
    +}): string[] {
    +  const dock = getChannelDock(params.channelId);
    +  if (dock?.config?.formatAllowFrom) {
    +    return dock.config.formatAllowFrom({
    +      cfg: params.cfg,
    +      accountId: params.accountId,
    +      allowFrom: params.values,
    +    });
    +  }
    +  return params.values.map((entry) => String(entry).trim()).filter(Boolean);
    +}
    +
    +function formatEntryList(entries: string[], resolved?: Map<string, string>): string {
    +  if (entries.length === 0) return "(none)";
    +  return entries
    +    .map((entry) => {
    +      const name = resolved?.get(entry);
    +      return name ? `${entry} (${name})` : entry;
    +    })
    +    .join(", ");
    +}
    +
    +function resolveAccountTarget(
    +  parsed: Record<string, unknown>,
    +  channelId: ChannelId,
    +  accountId?: string | null,
    +) {
    +  const channels = (parsed.channels ??= {}) as Record<string, unknown>;
    +  const channel = (channels[channelId] ??= {}) as Record<string, unknown>;
    +  const normalizedAccountId = normalizeAccountId(accountId);
    +  const hasAccounts = Boolean(channel.accounts && typeof channel.accounts === "object");
    +  const useAccount = normalizedAccountId !== DEFAULT_ACCOUNT_ID || hasAccounts;
    +  if (!useAccount) {
    +    return { target: channel, pathPrefix: `channels.${channelId}`, accountId: normalizedAccountId };
    +  }
    +  const accounts = (channel.accounts ??= {}) as Record<string, unknown>;
    +  const account = (accounts[normalizedAccountId] ??= {}) as Record<string, unknown>;
    +  return {
    +    target: account,
    +    pathPrefix: `channels.${channelId}.accounts.${normalizedAccountId}`,
    +    accountId: normalizedAccountId,
    +  };
    +}
    +
    +function getNestedValue(root: Record<string, unknown>, path: string[]): unknown {
    +  let current: unknown = root;
    +  for (const key of path) {
    +    if (!current || typeof current !== "object") return undefined;
    +    current = (current as Record<string, unknown>)[key];
    +  }
    +  return current;
    +}
    +
    +function ensureNestedObject(
    +  root: Record<string, unknown>,
    +  path: string[],
    +): Record<string, unknown> {
    +  let current = root;
    +  for (const key of path) {
    +    const existing = current[key];
    +    if (!existing || typeof existing !== "object") {
    +      current[key] = {};
    +    }
    +    current = current[key] as Record<string, unknown>;
    +  }
    +  return current;
    +}
    +
    +function setNestedValue(root: Record<string, unknown>, path: string[], value: unknown) {
    +  if (path.length === 0) return;
    +  if (path.length === 1) {
    +    root[path[0]] = value;
    +    return;
    +  }
    +  const parent = ensureNestedObject(root, path.slice(0, -1));
    +  parent[path[path.length - 1]] = value;
    +}
    +
    +function deleteNestedValue(root: Record<string, unknown>, path: string[]) {
    +  if (path.length === 0) return;
    +  if (path.length === 1) {
    +    delete root[path[0]];
    +    return;
    +  }
    +  const parent = getNestedValue(root, path.slice(0, -1));
    +  if (!parent || typeof parent !== "object") return;
    +  delete (parent as Record<string, unknown>)[path[path.length - 1]];
    +}
    +
    +function resolveChannelAllowFromPaths(
    +  channelId: ChannelId,
    +  scope: AllowlistScope,
    +): string[] | null {
    +  if (scope === "all") return null;
    +  if (scope === "dm") {
    +    if (channelId === "slack" || channelId === "discord") return ["dm", "allowFrom"];
    +    if (
    +      channelId === "telegram" ||
    +      channelId === "whatsapp" ||
    +      channelId === "signal" ||
    +      channelId === "imessage"
    +    ) {
    +      return ["allowFrom"];
    +    }
    +    return null;
    +  }
    +  if (scope === "group") {
    +    if (
    +      channelId === "telegram" ||
    +      channelId === "whatsapp" ||
    +      channelId === "signal" ||
    +      channelId === "imessage"
    +    ) {
    +      return ["groupAllowFrom"];
    +    }
    +    return null;
    +  }
    +  return null;
    +}
    +
    +async function resolveSlackNames(params: {
    +  cfg: ClawdbotConfig;
    +  accountId?: string | null;
    +  entries: string[];
    +}) {
    +  const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
    +  const token = account.config.userToken?.trim() || account.botToken?.trim();
    +  if (!token) return new Map<string, string>();
    +  const resolved = await resolveSlackUserAllowlist({ token, entries: params.entries });
    +  const map = new Map<string, string>();
    +  for (const entry of resolved) {
    +    if (entry.resolved && entry.name) map.set(entry.input, entry.name);
    +  }
    +  return map;
    +}
    +
    +async function resolveDiscordNames(params: {
    +  cfg: ClawdbotConfig;
    +  accountId?: string | null;
    +  entries: string[];
    +}) {
    +  const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
    +  const token = account.token?.trim();
    +  if (!token) return new Map<string, string>();
    +  const resolved = await resolveDiscordUserAllowlist({ token, entries: params.entries });
    +  const map = new Map<string, string>();
    +  for (const entry of resolved) {
    +    if (entry.resolved && entry.name) map.set(entry.input, entry.name);
    +  }
    +  return map;
    +}
    +
    +export const handleAllowlistCommand: CommandHandler = async (params, allowTextCommands) => {
    +  if (!allowTextCommands) return null;
    +  const parsed = parseAllowlistCommand(params.command.commandBodyNormalized);
    +  if (!parsed) return null;
    +  if (parsed.action === "error") {
    +    return { shouldContinue: false, reply: { text: `⚠️ ${parsed.message}` } };
    +  }
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /allowlist from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +
    +  const channelId =
    +    normalizeChannelId(parsed.channel) ??
    +    params.command.channelId ??
    +    normalizeChannelId(params.command.channel);
    +  if (!channelId) {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: "⚠️ Unknown channel. Add channel=<id> to the command." },
    +    };
    +  }
    +  const accountId = normalizeAccountId(parsed.account ?? params.ctx.AccountId);
    +  const scope = parsed.scope;
    +
    +  if (parsed.action === "list") {
    +    const pairingChannels = listPairingChannels();
    +    const supportsStore = pairingChannels.includes(channelId);
    +    const storeAllowFrom = supportsStore
    +      ? await readChannelAllowFromStore(channelId).catch(() => [])
    +      : [];
    +
    +    let dmAllowFrom: string[] = [];
    +    let groupAllowFrom: string[] = [];
    +    let groupOverrides: Array<{ label: string; entries: string[] }> = [];
    +    let dmPolicy: string | undefined;
    +    let groupPolicy: string | undefined;
    +
    +    if (channelId === "telegram") {
    +      const account = resolveTelegramAccount({ cfg: params.cfg, accountId });
    +      dmAllowFrom = (account.config.allowFrom ?? []).map(String);
    +      groupAllowFrom = (account.config.groupAllowFrom ?? []).map(String);
    +      dmPolicy = account.config.dmPolicy;
    +      groupPolicy = account.config.groupPolicy;
    +      const groups = account.config.groups ?? {};
    +      for (const [groupId, groupCfg] of Object.entries(groups)) {
    +        const entries = (groupCfg?.allowFrom ?? []).map(String).filter(Boolean);
    +        if (entries.length > 0) {
    +          groupOverrides.push({ label: groupId, entries });
    +        }
    +        const topics = groupCfg?.topics ?? {};
    +        for (const [topicId, topicCfg] of Object.entries(topics)) {
    +          const topicEntries = (topicCfg?.allowFrom ?? []).map(String).filter(Boolean);
    +          if (topicEntries.length > 0) {
    +            groupOverrides.push({ label: `${groupId} topic ${topicId}`, entries: topicEntries });
    +          }
    +        }
    +      }
    +    } else if (channelId === "whatsapp") {
    +      const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId });
    +      dmAllowFrom = (account.allowFrom ?? []).map(String);
    +      groupAllowFrom = (account.groupAllowFrom ?? []).map(String);
    +      dmPolicy = account.dmPolicy;
    +      groupPolicy = account.groupPolicy;
    +    } else if (channelId === "signal") {
    +      const account = resolveSignalAccount({ cfg: params.cfg, accountId });
    +      dmAllowFrom = (account.config.allowFrom ?? []).map(String);
    +      groupAllowFrom = (account.config.groupAllowFrom ?? []).map(String);
    +      dmPolicy = account.config.dmPolicy;
    +      groupPolicy = account.config.groupPolicy;
    +    } else if (channelId === "imessage") {
    +      const account = resolveIMessageAccount({ cfg: params.cfg, accountId });
    +      dmAllowFrom = (account.config.allowFrom ?? []).map(String);
    +      groupAllowFrom = (account.config.groupAllowFrom ?? []).map(String);
    +      dmPolicy = account.config.dmPolicy;
    +      groupPolicy = account.config.groupPolicy;
    +    } else if (channelId === "slack") {
    +      const account = resolveSlackAccount({ cfg: params.cfg, accountId });
    +      dmAllowFrom = (account.dm?.allowFrom ?? []).map(String);
    +      groupPolicy = account.groupPolicy;
    +      const channels = account.channels ?? {};
    +      groupOverrides = Object.entries(channels)
    +        .map(([key, value]) => {
    +          const entries = (value?.users ?? []).map(String).filter(Boolean);
    +          return entries.length > 0 ? { label: key, entries } : null;
    +        })
    +        .filter(Boolean) as Array<{ label: string; entries: string[] }>;
    +    } else if (channelId === "discord") {
    +      const account = resolveDiscordAccount({ cfg: params.cfg, accountId });
    +      dmAllowFrom = (account.config.dm?.allowFrom ?? []).map(String);
    +      groupPolicy = account.config.groupPolicy;
    +      const guilds = account.config.guilds ?? {};
    +      for (const [guildKey, guildCfg] of Object.entries(guilds)) {
    +        const entries = (guildCfg?.users ?? []).map(String).filter(Boolean);
    +        if (entries.length > 0) {
    +          groupOverrides.push({ label: `guild ${guildKey}`, entries });
    +        }
    +        const channels = guildCfg?.channels ?? {};
    +        for (const [channelKey, channelCfg] of Object.entries(channels)) {
    +          const channelEntries = (channelCfg?.users ?? []).map(String).filter(Boolean);
    +          if (channelEntries.length > 0) {
    +            groupOverrides.push({
    +              label: `guild ${guildKey} / channel ${channelKey}`,
    +              entries: channelEntries,
    +            });
    +          }
    +        }
    +      }
    +    }
    +
    +    const dmDisplay = normalizeAllowFrom({
    +      cfg: params.cfg,
    +      channelId,
    +      accountId,
    +      values: dmAllowFrom,
    +    });
    +    const groupDisplay = normalizeAllowFrom({
    +      cfg: params.cfg,
    +      channelId,
    +      accountId,
    +      values: groupAllowFrom,
    +    });
    +    const groupOverrideEntries = groupOverrides.flatMap((entry) => entry.entries);
    +    const groupOverrideDisplay = normalizeAllowFrom({
    +      cfg: params.cfg,
    +      channelId,
    +      accountId,
    +      values: groupOverrideEntries,
    +    });
    +    const resolvedDm =
    +      parsed.resolve && dmDisplay.length > 0 && channelId === "slack"
    +        ? await resolveSlackNames({ cfg: params.cfg, accountId, entries: dmDisplay })
    +        : parsed.resolve && dmDisplay.length > 0 && channelId === "discord"
    +          ? await resolveDiscordNames({ cfg: params.cfg, accountId, entries: dmDisplay })
    +          : undefined;
    +    const resolvedGroup =
    +      parsed.resolve && groupOverrideDisplay.length > 0 && channelId === "slack"
    +        ? await resolveSlackNames({
    +            cfg: params.cfg,
    +            accountId,
    +            entries: groupOverrideDisplay,
    +          })
    +        : parsed.resolve && groupOverrideDisplay.length > 0 && channelId === "discord"
    +          ? await resolveDiscordNames({
    +              cfg: params.cfg,
    +              accountId,
    +              entries: groupOverrideDisplay,
    +            })
    +          : undefined;
    +
    +    const lines: string[] = ["🧾 Allowlist"];
    +    lines.push(`Channel: ${channelId}${accountId ? ` (account ${accountId})` : ""}`);
    +    if (dmPolicy) lines.push(`DM policy: ${dmPolicy}`);
    +    if (groupPolicy) lines.push(`Group policy: ${groupPolicy}`);
    +
    +    const showDm = scope === "dm" || scope === "all";
    +    const showGroup = scope === "group" || scope === "all";
    +    if (showDm) {
    +      lines.push(`DM allowFrom (config): ${formatEntryList(dmDisplay, resolvedDm)}`);
    +    }
    +    if (supportsStore && storeAllowFrom.length > 0) {
    +      const storeLabel = normalizeAllowFrom({
    +        cfg: params.cfg,
    +        channelId,
    +        accountId,
    +        values: storeAllowFrom,
    +      });
    +      lines.push(`Paired allowFrom (store): ${formatEntryList(storeLabel)}`);
    +    }
    +    if (showGroup) {
    +      if (groupAllowFrom.length > 0) {
    +        lines.push(`Group allowFrom (config): ${formatEntryList(groupDisplay)}`);
    +      }
    +      if (groupOverrides.length > 0) {
    +        lines.push("Group overrides:");
    +        for (const entry of groupOverrides) {
    +          const normalized = normalizeAllowFrom({
    +            cfg: params.cfg,
    +            channelId,
    +            accountId,
    +            values: entry.entries,
    +          });
    +          lines.push(`- ${entry.label}: ${formatEntryList(normalized, resolvedGroup)}`);
    +        }
    +      }
    +    }
    +
    +    return { shouldContinue: false, reply: { text: lines.join("\n") } };
    +  }
    +
    +  if (params.cfg.commands?.config !== true) {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: "⚠️ /allowlist edits are disabled. Set commands.config=true to enable." },
    +    };
    +  }
    +
    +  const shouldUpdateConfig = parsed.target !== "store";
    +  const shouldTouchStore = parsed.target !== "config" && listPairingChannels().includes(channelId);
    +
    +  if (shouldUpdateConfig) {
    +    const allowWrites = resolveChannelConfigWrites({
    +      cfg: params.cfg,
    +      channelId,
    +      accountId: params.ctx.AccountId,
    +    });
    +    if (!allowWrites) {
    +      const hint = `channels.${channelId}.configWrites=true`;
    +      return {
    +        shouldContinue: false,
    +        reply: { text: `⚠️ Config writes are disabled for ${channelId}. Set ${hint} to enable.` },
    +      };
    +    }
    +
    +    const allowlistPath = resolveChannelAllowFromPaths(channelId, scope);
    +    if (!allowlistPath) {
    +      return {
    +        shouldContinue: false,
    +        reply: {
    +          text: `⚠️ ${channelId} does not support ${scope} allowlist edits via /allowlist.`,
    +        },
    +      };
    +    }
    +
    +    const snapshot = await readConfigFileSnapshot();
    +    if (!snapshot.valid || !snapshot.parsed || typeof snapshot.parsed !== "object") {
    +      return {
    +        shouldContinue: false,
    +        reply: { text: "⚠️ Config file is invalid; fix it before using /allowlist." },
    +      };
    +    }
    +    const parsedConfig = structuredClone(snapshot.parsed as Record<string, unknown>);
    +    const {
    +      target,
    +      pathPrefix,
    +      accountId: normalizedAccountId,
    +    } = resolveAccountTarget(parsedConfig, channelId, accountId);
    +    const existingRaw = getNestedValue(target, allowlistPath);
    +    const existing = Array.isArray(existingRaw)
    +      ? existingRaw.map((entry) => String(entry).trim()).filter(Boolean)
    +      : [];
    +
    +    const normalizedEntry = normalizeAllowFrom({
    +      cfg: params.cfg,
    +      channelId,
    +      accountId: normalizedAccountId,
    +      values: [parsed.entry],
    +    });
    +    if (normalizedEntry.length === 0) {
    +      return {
    +        shouldContinue: false,
    +        reply: { text: "⚠️ Invalid allowlist entry." },
    +      };
    +    }
    +
    +    const existingNormalized = normalizeAllowFrom({
    +      cfg: params.cfg,
    +      channelId,
    +      accountId: normalizedAccountId,
    +      values: existing,
    +    });
    +
    +    const shouldMatch = (value: string) => normalizedEntry.includes(value);
    +
    +    let configChanged = false;
    +    let next = existing;
    +    const configHasEntry = existingNormalized.some((value) => shouldMatch(value));
    +    if (parsed.action === "add") {
    +      if (!configHasEntry) {
    +        next = [...existing, parsed.entry.trim()];
    +        configChanged = true;
    +      }
    +    }
    +
    +    if (parsed.action === "remove") {
    +      const keep: string[] = [];
    +      for (const entry of existing) {
    +        const normalized = normalizeAllowFrom({
    +          cfg: params.cfg,
    +          channelId,
    +          accountId: normalizedAccountId,
    +          values: [entry],
    +        });
    +        if (normalized.some((value) => shouldMatch(value))) {
    +          configChanged = true;
    +          continue;
    +        }
    +        keep.push(entry);
    +      }
    +      next = keep;
    +    }
    +
    +    if (configChanged) {
    +      if (next.length === 0) {
    +        deleteNestedValue(target, allowlistPath);
    +      } else {
    +        setNestedValue(target, allowlistPath, next);
    +      }
    +    }
    +
    +    if (configChanged) {
    +      const validated = validateConfigObjectWithPlugins(parsedConfig);
    +      if (!validated.ok) {
    +        const issue = validated.issues[0];
    +        return {
    +          shouldContinue: false,
    +          reply: { text: `⚠️ Config invalid after update (${issue.path}: ${issue.message}).` },
    +        };
    +      }
    +      await writeConfigFile(validated.config);
    +    }
    +
    +    if (!configChanged && !shouldTouchStore) {
    +      const message = parsed.action === "add" ? "✅ Already allowlisted." : "⚠️ Entry not found.";
    +      return { shouldContinue: false, reply: { text: message } };
    +    }
    +
    +    if (shouldTouchStore) {
    +      if (parsed.action === "add") {
    +        await addChannelAllowFromStoreEntry({ channel: channelId, entry: parsed.entry });
    +      } else if (parsed.action === "remove") {
    +        await removeChannelAllowFromStoreEntry({ channel: channelId, entry: parsed.entry });
    +      }
    +    }
    +
    +    const actionLabel = parsed.action === "add" ? "added" : "removed";
    +    const scopeLabel = scope === "dm" ? "DM" : "group";
    +    const locations: string[] = [];
    +    if (configChanged) {
    +      locations.push(`${pathPrefix}.${allowlistPath.join(".")}`);
    +    }
    +    if (shouldTouchStore) {
    +      locations.push("pairing store");
    +    }
    +    const targetLabel = locations.length > 0 ? locations.join(" + ") : "no-op";
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: `✅ ${scopeLabel} allowlist ${actionLabel}: ${targetLabel}.`,
    +      },
    +    };
    +  }
    +
    +  if (!shouldTouchStore) {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: "⚠️ This channel does not support allowlist storage." },
    +    };
    +  }
    +
    +  if (parsed.action === "add") {
    +    await addChannelAllowFromStoreEntry({ channel: channelId, entry: parsed.entry });
    +  } else if (parsed.action === "remove") {
    +    await removeChannelAllowFromStoreEntry({ channel: channelId, entry: parsed.entry });
    +  }
    +
    +  const actionLabel = parsed.action === "add" ? "added" : "removed";
    +  const scopeLabel = scope === "dm" ? "DM" : "group";
    +  return {
    +    shouldContinue: false,
    +    reply: { text: `✅ ${scopeLabel} allowlist ${actionLabel} in pairing store.` },
    +  };
    +};
    
  • src/auto-reply/reply/commands-core.ts+2 0 modified
    @@ -13,6 +13,7 @@ import {
       handleStatusCommand,
       handleWhoamiCommand,
     } from "./commands-info.js";
    +import { handleAllowlistCommand } from "./commands-allowlist.js";
     import { handleSubagentsCommand } from "./commands-subagents.js";
     import {
       handleAbortTrigger,
    @@ -37,6 +38,7 @@ const HANDLERS: CommandHandler[] = [
       handleHelpCommand,
       handleCommandsListCommand,
       handleStatusCommand,
    +  handleAllowlistCommand,
       handleContextCommand,
       handleWhoamiCommand,
       handleSubagentsCommand,
    
  • src/pairing/pairing-store.ts+31 0 modified
    @@ -245,6 +245,37 @@ export async function addChannelAllowFromStoreEntry(params: {
       );
     }
     
    +export async function removeChannelAllowFromStoreEntry(params: {
    +  channel: PairingChannel;
    +  entry: string | number;
    +  env?: NodeJS.ProcessEnv;
    +}): Promise<{ changed: boolean; allowFrom: string[] }> {
    +  const env = params.env ?? process.env;
    +  const filePath = resolveAllowFromPath(params.channel, env);
    +  return await withFileLock(
    +    filePath,
    +    { version: 1, allowFrom: [] } satisfies AllowFromStore,
    +    async () => {
    +      const { value } = await readJsonFile<AllowFromStore>(filePath, {
    +        version: 1,
    +        allowFrom: [],
    +      });
    +      const current = (Array.isArray(value.allowFrom) ? value.allowFrom : [])
    +        .map((v) => normalizeAllowEntry(params.channel, String(v)))
    +        .filter(Boolean);
    +      const normalized = normalizeAllowEntry(params.channel, normalizeId(params.entry));
    +      if (!normalized) return { changed: false, allowFrom: current };
    +      const next = current.filter((entry) => entry !== normalized);
    +      if (next.length === current.length) return { changed: false, allowFrom: current };
    +      await writeJsonFile(filePath, {
    +        version: 1,
    +        allowFrom: next,
    +      } satisfies AllowFromStore);
    +      return { changed: true, allowFrom: next };
    +    },
    +  );
    +}
    +
     export async function listChannelPairingRequests(
       channel: PairingChannel,
       env: NodeJS.ProcessEnv = process.env,
    
ea018a68ccb9

refactor(auto-reply): split reply pipeline

https://github.com/openclaw/openclawPeter SteinbergerJan 14, 2026via ghsa
26 files changed · +4381 3286
  • src/auto-reply/reply/agent-runner-execution.ts+475 0 added
    @@ -0,0 +1,475 @@
    +import crypto from "node:crypto";
    +import fs from "node:fs";
    +import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
    +import { runCliAgent } from "../../agents/cli-runner.js";
    +import { getCliSessionId } from "../../agents/cli-session.js";
    +import { runWithModelFallback } from "../../agents/model-fallback.js";
    +import { isCliProvider } from "../../agents/model-selection.js";
    +import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
    +import {
    +  isCompactionFailureError,
    +  isContextOverflowError,
    +} from "../../agents/pi-embedded-helpers.js";
    +import {
    +  resolveAgentIdFromSessionKey,
    +  resolveSessionTranscriptPath,
    +  type SessionEntry,
    +  saveSessionStore,
    +} from "../../config/sessions.js";
    +import { logVerbose } from "../../globals.js";
    +import {
    +  emitAgentEvent,
    +  registerAgentRunContext,
    +} from "../../infra/agent-events.js";
    +import { defaultRuntime } from "../../runtime.js";
    +import { stripHeartbeatToken } from "../heartbeat.js";
    +import type { TemplateContext } from "../templating.js";
    +import type { VerboseLevel } from "../thinking.js";
    +import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
    +import type { GetReplyOptions, ReplyPayload } from "../types.js";
    +import {
    +  buildThreadingToolContext,
    +  resolveEnforceFinalTag,
    +} from "./agent-runner-utils.js";
    +import type { BlockReplyPipeline } from "./block-reply-pipeline.js";
    +import type { FollowupRun } from "./queue.js";
    +import { parseReplyDirectives } from "./reply-directives.js";
    +import {
    +  applyReplyTagsToPayload,
    +  isRenderablePayload,
    +} from "./reply-payloads.js";
    +import type { TypingSignaler } from "./typing-mode.js";
    +
    +export type AgentRunLoopResult =
    +  | {
    +      kind: "success";
    +      runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
    +      fallbackProvider?: string;
    +      fallbackModel?: string;
    +      didLogHeartbeatStrip: boolean;
    +      autoCompactionCompleted: boolean;
    +    }
    +  | { kind: "final"; payload: ReplyPayload };
    +
    +export async function runAgentTurnWithFallback(params: {
    +  commandBody: string;
    +  followupRun: FollowupRun;
    +  sessionCtx: TemplateContext;
    +  opts?: GetReplyOptions;
    +  typingSignals: TypingSignaler;
    +  blockReplyPipeline: BlockReplyPipeline | null;
    +  blockStreamingEnabled: boolean;
    +  blockReplyChunking?: {
    +    minChars: number;
    +    maxChars: number;
    +    breakPreference: "paragraph" | "newline" | "sentence";
    +  };
    +  resolvedBlockStreamingBreak: "text_end" | "message_end";
    +  applyReplyToMode: (payload: ReplyPayload) => ReplyPayload;
    +  shouldEmitToolResult: () => boolean;
    +  pendingToolTasks: Set<Promise<void>>;
    +  resetSessionAfterCompactionFailure: (reason: string) => Promise<boolean>;
    +  isHeartbeat: boolean;
    +  sessionKey?: string;
    +  getActiveSessionEntry: () => SessionEntry | undefined;
    +  activeSessionStore?: Record<string, SessionEntry>;
    +  storePath?: string;
    +  resolvedVerboseLevel: VerboseLevel;
    +}): Promise<AgentRunLoopResult> {
    +  let didLogHeartbeatStrip = false;
    +  let autoCompactionCompleted = false;
    +
    +  const runId = crypto.randomUUID();
    +  if (params.sessionKey) {
    +    registerAgentRunContext(runId, {
    +      sessionKey: params.sessionKey,
    +      verboseLevel: params.resolvedVerboseLevel,
    +    });
    +  }
    +  let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
    +  let fallbackProvider = params.followupRun.run.provider;
    +  let fallbackModel = params.followupRun.run.model;
    +  let didResetAfterCompactionFailure = false;
    +
    +  while (true) {
    +    try {
    +      const allowPartialStream = !(
    +        params.followupRun.run.reasoningLevel === "stream" &&
    +        params.opts?.onReasoningStream
    +      );
    +      const normalizeStreamingText = (
    +        payload: ReplyPayload,
    +      ): { text?: string; skip: boolean } => {
    +        if (!allowPartialStream) return { skip: true };
    +        let text = payload.text;
    +        if (!params.isHeartbeat && text?.includes("HEARTBEAT_OK")) {
    +          const stripped = stripHeartbeatToken(text, {
    +            mode: "message",
    +          });
    +          if (stripped.didStrip && !didLogHeartbeatStrip) {
    +            didLogHeartbeatStrip = true;
    +            logVerbose("Stripped stray HEARTBEAT_OK token from reply");
    +          }
    +          if (stripped.shouldSkip && (payload.mediaUrls?.length ?? 0) === 0) {
    +            return { skip: true };
    +          }
    +          text = stripped.text;
    +        }
    +        if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
    +          return { skip: true };
    +        }
    +        return { text, skip: false };
    +      };
    +      const handlePartialForTyping = async (
    +        payload: ReplyPayload,
    +      ): Promise<string | undefined> => {
    +        const { text, skip } = normalizeStreamingText(payload);
    +        if (skip || !text) return undefined;
    +        await params.typingSignals.signalTextDelta(text);
    +        return text;
    +      };
    +      const blockReplyPipeline = params.blockReplyPipeline;
    +      const onToolResult = params.opts?.onToolResult;
    +      const fallbackResult = await runWithModelFallback({
    +        cfg: params.followupRun.run.config,
    +        provider: params.followupRun.run.provider,
    +        model: params.followupRun.run.model,
    +        fallbacksOverride: resolveAgentModelFallbacksOverride(
    +          params.followupRun.run.config,
    +          resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey),
    +        ),
    +        run: (provider, model) => {
    +          if (isCliProvider(provider, params.followupRun.run.config)) {
    +            const startedAt = Date.now();
    +            emitAgentEvent({
    +              runId,
    +              stream: "lifecycle",
    +              data: {
    +                phase: "start",
    +                startedAt,
    +              },
    +            });
    +            const cliSessionId = getCliSessionId(
    +              params.getActiveSessionEntry(),
    +              provider,
    +            );
    +            return runCliAgent({
    +              sessionId: params.followupRun.run.sessionId,
    +              sessionKey: params.sessionKey,
    +              sessionFile: params.followupRun.run.sessionFile,
    +              workspaceDir: params.followupRun.run.workspaceDir,
    +              config: params.followupRun.run.config,
    +              prompt: params.commandBody,
    +              provider,
    +              model,
    +              thinkLevel: params.followupRun.run.thinkLevel,
    +              timeoutMs: params.followupRun.run.timeoutMs,
    +              runId,
    +              extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
    +              ownerNumbers: params.followupRun.run.ownerNumbers,
    +              cliSessionId,
    +            })
    +              .then((result) => {
    +                emitAgentEvent({
    +                  runId,
    +                  stream: "lifecycle",
    +                  data: {
    +                    phase: "end",
    +                    startedAt,
    +                    endedAt: Date.now(),
    +                  },
    +                });
    +                return result;
    +              })
    +              .catch((err) => {
    +                emitAgentEvent({
    +                  runId,
    +                  stream: "lifecycle",
    +                  data: {
    +                    phase: "error",
    +                    startedAt,
    +                    endedAt: Date.now(),
    +                    error: err instanceof Error ? err.message : String(err),
    +                  },
    +                });
    +                throw err;
    +              });
    +          }
    +          return runEmbeddedPiAgent({
    +            sessionId: params.followupRun.run.sessionId,
    +            sessionKey: params.sessionKey,
    +            messageProvider:
    +              params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
    +            agentAccountId: params.sessionCtx.AccountId,
    +            // Provider threading context for tool auto-injection
    +            ...buildThreadingToolContext({
    +              sessionCtx: params.sessionCtx,
    +              config: params.followupRun.run.config,
    +              hasRepliedRef: params.opts?.hasRepliedRef,
    +            }),
    +            sessionFile: params.followupRun.run.sessionFile,
    +            workspaceDir: params.followupRun.run.workspaceDir,
    +            agentDir: params.followupRun.run.agentDir,
    +            config: params.followupRun.run.config,
    +            skillsSnapshot: params.followupRun.run.skillsSnapshot,
    +            prompt: params.commandBody,
    +            extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
    +            ownerNumbers: params.followupRun.run.ownerNumbers,
    +            enforceFinalTag: resolveEnforceFinalTag(
    +              params.followupRun.run,
    +              provider,
    +            ),
    +            provider,
    +            model,
    +            authProfileId: params.followupRun.run.authProfileId,
    +            thinkLevel: params.followupRun.run.thinkLevel,
    +            verboseLevel: params.followupRun.run.verboseLevel,
    +            reasoningLevel: params.followupRun.run.reasoningLevel,
    +            bashElevated: params.followupRun.run.bashElevated,
    +            timeoutMs: params.followupRun.run.timeoutMs,
    +            runId,
    +            blockReplyBreak: params.resolvedBlockStreamingBreak,
    +            blockReplyChunking: params.blockReplyChunking,
    +            onPartialReply: allowPartialStream
    +              ? async (payload) => {
    +                  const textForTyping = await handlePartialForTyping(payload);
    +                  if (
    +                    !params.opts?.onPartialReply ||
    +                    textForTyping === undefined
    +                  )
    +                    return;
    +                  await params.opts.onPartialReply({
    +                    text: textForTyping,
    +                    mediaUrls: payload.mediaUrls,
    +                  });
    +                }
    +              : undefined,
    +            onAssistantMessageStart: async () => {
    +              await params.typingSignals.signalMessageStart();
    +            },
    +            onReasoningStream:
    +              params.typingSignals.shouldStartOnReasoning ||
    +              params.opts?.onReasoningStream
    +                ? async (payload) => {
    +                    await params.typingSignals.signalReasoningDelta();
    +                    await params.opts?.onReasoningStream?.({
    +                      text: payload.text,
    +                      mediaUrls: payload.mediaUrls,
    +                    });
    +                  }
    +                : undefined,
    +            onAgentEvent: (evt) => {
    +              // Trigger typing when tools start executing
    +              if (evt.stream === "tool") {
    +                const phase =
    +                  typeof evt.data.phase === "string" ? evt.data.phase : "";
    +                if (phase === "start" || phase === "update") {
    +                  void params.typingSignals.signalToolStart();
    +                }
    +              }
    +              // Track auto-compaction completion
    +              if (evt.stream === "compaction") {
    +                const phase =
    +                  typeof evt.data.phase === "string" ? evt.data.phase : "";
    +                const willRetry = Boolean(evt.data.willRetry);
    +                if (phase === "end" && !willRetry) {
    +                  autoCompactionCompleted = true;
    +                }
    +              }
    +            },
    +            onBlockReply:
    +              params.blockStreamingEnabled && params.opts?.onBlockReply
    +                ? async (payload) => {
    +                    const { text, skip } = normalizeStreamingText(payload);
    +                    const hasPayloadMedia =
    +                      (payload.mediaUrls?.length ?? 0) > 0;
    +                    if (skip && !hasPayloadMedia) return;
    +                    const taggedPayload = applyReplyTagsToPayload(
    +                      {
    +                        text,
    +                        mediaUrls: payload.mediaUrls,
    +                        mediaUrl: payload.mediaUrls?.[0],
    +                      },
    +                      params.sessionCtx.MessageSid,
    +                    );
    +                    // Let through payloads with audioAsVoice flag even if empty (need to track it)
    +                    if (
    +                      !isRenderablePayload(taggedPayload) &&
    +                      !payload.audioAsVoice
    +                    )
    +                      return;
    +                    const parsed = parseReplyDirectives(
    +                      taggedPayload.text ?? "",
    +                      {
    +                        currentMessageId: params.sessionCtx.MessageSid,
    +                        silentToken: SILENT_REPLY_TOKEN,
    +                      },
    +                    );
    +                    const cleaned = parsed.text || undefined;
    +                    const hasRenderableMedia =
    +                      Boolean(taggedPayload.mediaUrl) ||
    +                      (taggedPayload.mediaUrls?.length ?? 0) > 0;
    +                    // Skip empty payloads unless they have audioAsVoice flag (need to track it)
    +                    if (
    +                      !cleaned &&
    +                      !hasRenderableMedia &&
    +                      !payload.audioAsVoice &&
    +                      !parsed.audioAsVoice
    +                    )
    +                      return;
    +                    if (parsed.isSilent && !hasRenderableMedia) return;
    +
    +                    const blockPayload: ReplyPayload = params.applyReplyToMode({
    +                      ...taggedPayload,
    +                      text: cleaned,
    +                      audioAsVoice: Boolean(
    +                        parsed.audioAsVoice || payload.audioAsVoice,
    +                      ),
    +                      replyToId: taggedPayload.replyToId ?? parsed.replyToId,
    +                      replyToTag: taggedPayload.replyToTag || parsed.replyToTag,
    +                      replyToCurrent:
    +                        taggedPayload.replyToCurrent || parsed.replyToCurrent,
    +                    });
    +
    +                    void params.typingSignals
    +                      .signalTextDelta(cleaned ?? taggedPayload.text)
    +                      .catch((err) => {
    +                        logVerbose(
    +                          `block reply typing signal failed: ${String(err)}`,
    +                        );
    +                      });
    +
    +                    params.blockReplyPipeline?.enqueue(blockPayload);
    +                  }
    +                : undefined,
    +            onBlockReplyFlush:
    +              params.blockStreamingEnabled && blockReplyPipeline
    +                ? async () => {
    +                    await blockReplyPipeline.flush({ force: true });
    +                  }
    +                : undefined,
    +            shouldEmitToolResult: params.shouldEmitToolResult,
    +            onToolResult: onToolResult
    +              ? (payload) => {
    +                  // `subscribeEmbeddedPiSession` may invoke tool callbacks without awaiting them.
    +                  // If a tool callback starts typing after the run finalized, we can end up with
    +                  // a typing loop that never sees a matching markRunComplete(). Track and drain.
    +                  const task = (async () => {
    +                    const { text, skip } = normalizeStreamingText(payload);
    +                    if (skip) return;
    +                    await params.typingSignals.signalTextDelta(text);
    +                    await onToolResult({
    +                      text,
    +                      mediaUrls: payload.mediaUrls,
    +                    });
    +                  })()
    +                    .catch((err) => {
    +                      logVerbose(`tool result delivery failed: ${String(err)}`);
    +                    })
    +                    .finally(() => {
    +                      params.pendingToolTasks.delete(task);
    +                    });
    +                  params.pendingToolTasks.add(task);
    +                }
    +              : undefined,
    +          });
    +        },
    +      });
    +      runResult = fallbackResult.result;
    +      fallbackProvider = fallbackResult.provider;
    +      fallbackModel = fallbackResult.model;
    +
    +      // Some embedded runs surface context overflow as an error payload instead of throwing.
    +      // Treat those as a session-level failure and auto-recover by starting a fresh session.
    +      const embeddedError = runResult.meta?.error;
    +      if (
    +        embeddedError &&
    +        isContextOverflowError(embeddedError.message) &&
    +        !didResetAfterCompactionFailure &&
    +        (await params.resetSessionAfterCompactionFailure(embeddedError.message))
    +      ) {
    +        didResetAfterCompactionFailure = true;
    +        continue;
    +      }
    +
    +      break;
    +    } catch (err) {
    +      const message = err instanceof Error ? err.message : String(err);
    +      const isContextOverflow =
    +        isContextOverflowError(message) ||
    +        /context.*overflow|too large|context window/i.test(message);
    +      const isCompactionFailure = isCompactionFailureError(message);
    +      const isSessionCorruption =
    +        /function call turn comes immediately after/i.test(message);
    +
    +      if (
    +        isCompactionFailure &&
    +        !didResetAfterCompactionFailure &&
    +        (await params.resetSessionAfterCompactionFailure(message))
    +      ) {
    +        didResetAfterCompactionFailure = true;
    +        continue;
    +      }
    +
    +      // Auto-recover from Gemini session corruption by resetting the session
    +      if (
    +        isSessionCorruption &&
    +        params.sessionKey &&
    +        params.activeSessionStore &&
    +        params.storePath
    +      ) {
    +        const corruptedSessionId = params.getActiveSessionEntry()?.sessionId;
    +        defaultRuntime.error(
    +          `Session history corrupted (Gemini function call ordering). Resetting session: ${params.sessionKey}`,
    +        );
    +
    +        try {
    +          // Delete transcript file if it exists
    +          if (corruptedSessionId) {
    +            const transcriptPath =
    +              resolveSessionTranscriptPath(corruptedSessionId);
    +            try {
    +              fs.unlinkSync(transcriptPath);
    +            } catch {
    +              // Ignore if file doesn't exist
    +            }
    +          }
    +
    +          // Remove session entry from store
    +          delete params.activeSessionStore[params.sessionKey];
    +          await saveSessionStore(params.storePath, params.activeSessionStore);
    +        } catch (cleanupErr) {
    +          defaultRuntime.error(
    +            `Failed to reset corrupted session ${params.sessionKey}: ${String(cleanupErr)}`,
    +          );
    +        }
    +
    +        return {
    +          kind: "final",
    +          payload: {
    +            text: "⚠️ Session history was corrupted. I've reset the conversation - please try again!",
    +          },
    +        };
    +      }
    +
    +      defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
    +      return {
    +        kind: "final",
    +        payload: {
    +          text: isContextOverflow
    +            ? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
    +            : `⚠️ Agent failed before reply: ${message}. Check gateway logs for details.`,
    +        },
    +      };
    +    }
    +  }
    +
    +  return {
    +    kind: "success",
    +    runResult,
    +    fallbackProvider,
    +    fallbackModel,
    +    didLogHeartbeatStrip,
    +    autoCompactionCompleted,
    +  };
    +}
    
  • src/auto-reply/reply/agent-runner-helpers.ts+60 0 added
    @@ -0,0 +1,60 @@
    +import { loadSessionStore } from "../../config/sessions.js";
    +import { isAudioFileName } from "../../media/mime.js";
    +import { normalizeVerboseLevel, type VerboseLevel } from "../thinking.js";
    +import type { ReplyPayload } from "../types.js";
    +import { scheduleFollowupDrain } from "./queue.js";
    +import type { TypingSignaler } from "./typing-mode.js";
    +
    +const hasAudioMedia = (urls?: string[]): boolean =>
    +  Boolean(urls?.some((url) => isAudioFileName(url)));
    +
    +export const isAudioPayload = (payload: ReplyPayload): boolean =>
    +  hasAudioMedia(
    +    payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined),
    +  );
    +
    +export const createShouldEmitToolResult = (params: {
    +  sessionKey?: string;
    +  storePath?: string;
    +  resolvedVerboseLevel: VerboseLevel;
    +}): (() => boolean) => {
    +  return () => {
    +    if (!params.sessionKey || !params.storePath) {
    +      return params.resolvedVerboseLevel === "on";
    +    }
    +    try {
    +      const store = loadSessionStore(params.storePath);
    +      const entry = store[params.sessionKey];
    +      const current = normalizeVerboseLevel(entry?.verboseLevel);
    +      if (current) return current === "on";
    +    } catch {
    +      // ignore store read failures
    +    }
    +    return params.resolvedVerboseLevel === "on";
    +  };
    +};
    +
    +export const finalizeWithFollowup = <T>(
    +  value: T,
    +  queueKey: string,
    +  runFollowupTurn: Parameters<typeof scheduleFollowupDrain>[1],
    +): T => {
    +  scheduleFollowupDrain(queueKey, runFollowupTurn);
    +  return value;
    +};
    +
    +export const signalTypingIfNeeded = async (
    +  payloads: ReplyPayload[],
    +  typingSignals: TypingSignaler,
    +): Promise<void> => {
    +  const shouldSignalTyping = payloads.some((payload) => {
    +    const trimmed = payload.text?.trim();
    +    if (trimmed) return true;
    +    if (payload.mediaUrl) return true;
    +    if (payload.mediaUrls && payload.mediaUrls.length > 0) return true;
    +    return false;
    +  });
    +  if (shouldSignalTyping) {
    +    await typingSignals.signalRunStart();
    +  }
    +};
    
  • src/auto-reply/reply/agent-runner-memory.ts+195 0 added
    @@ -0,0 +1,195 @@
    +import crypto from "node:crypto";
    +import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
    +import { runWithModelFallback } from "../../agents/model-fallback.js";
    +import { isCliProvider } from "../../agents/model-selection.js";
    +import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
    +import {
    +  resolveSandboxConfigForAgent,
    +  resolveSandboxRuntimeStatus,
    +} from "../../agents/sandbox.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import {
    +  resolveAgentIdFromSessionKey,
    +  type SessionEntry,
    +  updateSessionStoreEntry,
    +} from "../../config/sessions.js";
    +import { logVerbose } from "../../globals.js";
    +import { registerAgentRunContext } from "../../infra/agent-events.js";
    +import type { TemplateContext } from "../templating.js";
    +import type { VerboseLevel } from "../thinking.js";
    +import type { GetReplyOptions } from "../types.js";
    +import {
    +  buildThreadingToolContext,
    +  resolveEnforceFinalTag,
    +} from "./agent-runner-utils.js";
    +import {
    +  resolveMemoryFlushContextWindowTokens,
    +  resolveMemoryFlushSettings,
    +  shouldRunMemoryFlush,
    +} from "./memory-flush.js";
    +import type { FollowupRun } from "./queue.js";
    +import { incrementCompactionCount } from "./session-updates.js";
    +
    +export async function runMemoryFlushIfNeeded(params: {
    +  cfg: ClawdbotConfig;
    +  followupRun: FollowupRun;
    +  sessionCtx: TemplateContext;
    +  opts?: GetReplyOptions;
    +  defaultModel: string;
    +  agentCfgContextTokens?: number;
    +  resolvedVerboseLevel: VerboseLevel;
    +  sessionEntry?: SessionEntry;
    +  sessionStore?: Record<string, SessionEntry>;
    +  sessionKey?: string;
    +  storePath?: string;
    +  isHeartbeat: boolean;
    +}): Promise<SessionEntry | undefined> {
    +  const memoryFlushSettings = resolveMemoryFlushSettings(params.cfg);
    +  if (!memoryFlushSettings) return params.sessionEntry;
    +
    +  const memoryFlushWritable = (() => {
    +    if (!params.sessionKey) return true;
    +    const runtime = resolveSandboxRuntimeStatus({
    +      cfg: params.cfg,
    +      sessionKey: params.sessionKey,
    +    });
    +    if (!runtime.sandboxed) return true;
    +    const sandboxCfg = resolveSandboxConfigForAgent(
    +      params.cfg,
    +      runtime.agentId,
    +    );
    +    return sandboxCfg.workspaceAccess === "rw";
    +  })();
    +
    +  const shouldFlushMemory =
    +    memoryFlushSettings &&
    +    memoryFlushWritable &&
    +    !params.isHeartbeat &&
    +    !isCliProvider(params.followupRun.run.provider, params.cfg) &&
    +    shouldRunMemoryFlush({
    +      entry:
    +        params.sessionEntry ??
    +        (params.sessionKey
    +          ? params.sessionStore?.[params.sessionKey]
    +          : undefined),
    +      contextWindowTokens: resolveMemoryFlushContextWindowTokens({
    +        modelId: params.followupRun.run.model ?? params.defaultModel,
    +        agentCfgContextTokens: params.agentCfgContextTokens,
    +      }),
    +      reserveTokensFloor: memoryFlushSettings.reserveTokensFloor,
    +      softThresholdTokens: memoryFlushSettings.softThresholdTokens,
    +    });
    +
    +  if (!shouldFlushMemory) return params.sessionEntry;
    +
    +  let activeSessionEntry = params.sessionEntry;
    +  const activeSessionStore = params.sessionStore;
    +  const flushRunId = crypto.randomUUID();
    +  if (params.sessionKey) {
    +    registerAgentRunContext(flushRunId, {
    +      sessionKey: params.sessionKey,
    +      verboseLevel: params.resolvedVerboseLevel,
    +    });
    +  }
    +  let memoryCompactionCompleted = false;
    +  const flushSystemPrompt = [
    +    params.followupRun.run.extraSystemPrompt,
    +    memoryFlushSettings.systemPrompt,
    +  ]
    +    .filter(Boolean)
    +    .join("\n\n");
    +  try {
    +    await runWithModelFallback({
    +      cfg: params.followupRun.run.config,
    +      provider: params.followupRun.run.provider,
    +      model: params.followupRun.run.model,
    +      fallbacksOverride: resolveAgentModelFallbacksOverride(
    +        params.followupRun.run.config,
    +        resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey),
    +      ),
    +      run: (provider, model) =>
    +        runEmbeddedPiAgent({
    +          sessionId: params.followupRun.run.sessionId,
    +          sessionKey: params.sessionKey,
    +          messageProvider:
    +            params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
    +          agentAccountId: params.sessionCtx.AccountId,
    +          // Provider threading context for tool auto-injection
    +          ...buildThreadingToolContext({
    +            sessionCtx: params.sessionCtx,
    +            config: params.followupRun.run.config,
    +            hasRepliedRef: params.opts?.hasRepliedRef,
    +          }),
    +          sessionFile: params.followupRun.run.sessionFile,
    +          workspaceDir: params.followupRun.run.workspaceDir,
    +          agentDir: params.followupRun.run.agentDir,
    +          config: params.followupRun.run.config,
    +          skillsSnapshot: params.followupRun.run.skillsSnapshot,
    +          prompt: memoryFlushSettings.prompt,
    +          extraSystemPrompt: flushSystemPrompt,
    +          ownerNumbers: params.followupRun.run.ownerNumbers,
    +          enforceFinalTag: resolveEnforceFinalTag(
    +            params.followupRun.run,
    +            provider,
    +          ),
    +          provider,
    +          model,
    +          authProfileId: params.followupRun.run.authProfileId,
    +          thinkLevel: params.followupRun.run.thinkLevel,
    +          verboseLevel: params.followupRun.run.verboseLevel,
    +          reasoningLevel: params.followupRun.run.reasoningLevel,
    +          bashElevated: params.followupRun.run.bashElevated,
    +          timeoutMs: params.followupRun.run.timeoutMs,
    +          runId: flushRunId,
    +          onAgentEvent: (evt) => {
    +            if (evt.stream === "compaction") {
    +              const phase =
    +                typeof evt.data.phase === "string" ? evt.data.phase : "";
    +              const willRetry = Boolean(evt.data.willRetry);
    +              if (phase === "end" && !willRetry) {
    +                memoryCompactionCompleted = true;
    +              }
    +            }
    +          },
    +        }),
    +    });
    +    let memoryFlushCompactionCount =
    +      activeSessionEntry?.compactionCount ??
    +      (params.sessionKey
    +        ? activeSessionStore?.[params.sessionKey]?.compactionCount
    +        : 0) ??
    +      0;
    +    if (memoryCompactionCompleted) {
    +      const nextCount = await incrementCompactionCount({
    +        sessionEntry: activeSessionEntry,
    +        sessionStore: activeSessionStore,
    +        sessionKey: params.sessionKey,
    +        storePath: params.storePath,
    +      });
    +      if (typeof nextCount === "number") {
    +        memoryFlushCompactionCount = nextCount;
    +      }
    +    }
    +    if (params.storePath && params.sessionKey) {
    +      try {
    +        const updatedEntry = await updateSessionStoreEntry({
    +          storePath: params.storePath,
    +          sessionKey: params.sessionKey,
    +          update: async () => ({
    +            memoryFlushAt: Date.now(),
    +            memoryFlushCompactionCount,
    +          }),
    +        });
    +        if (updatedEntry) {
    +          activeSessionEntry = updatedEntry;
    +        }
    +      } catch (err) {
    +        logVerbose(`failed to persist memory flush metadata: ${String(err)}`);
    +      }
    +    }
    +  } catch (err) {
    +    logVerbose(`memory flush run failed: ${String(err)}`);
    +  }
    +
    +  return activeSessionEntry;
    +}
    
  • src/auto-reply/reply/agent-runner-payloads.ts+118 0 added
    @@ -0,0 +1,118 @@
    +import type { ReplyToMode } from "../../config/types.js";
    +import { logVerbose } from "../../globals.js";
    +import { stripHeartbeatToken } from "../heartbeat.js";
    +import type { OriginatingChannelType } from "../templating.js";
    +import { SILENT_REPLY_TOKEN } from "../tokens.js";
    +import type { ReplyPayload } from "../types.js";
    +import {
    +  formatBunFetchSocketError,
    +  isBunFetchSocketError,
    +} from "./agent-runner-utils.js";
    +import type { BlockReplyPipeline } from "./block-reply-pipeline.js";
    +import { parseReplyDirectives } from "./reply-directives.js";
    +import {
    +  applyReplyThreading,
    +  filterMessagingToolDuplicates,
    +  isRenderablePayload,
    +  shouldSuppressMessagingToolReplies,
    +} from "./reply-payloads.js";
    +
    +export function buildReplyPayloads(params: {
    +  payloads: ReplyPayload[];
    +  isHeartbeat: boolean;
    +  didLogHeartbeatStrip: boolean;
    +  blockStreamingEnabled: boolean;
    +  blockReplyPipeline: BlockReplyPipeline | null;
    +  replyToMode: ReplyToMode;
    +  replyToChannel?: OriginatingChannelType;
    +  currentMessageId?: string;
    +  messageProvider?: string;
    +  messagingToolSentTexts?: string[];
    +  messagingToolSentTargets?: Parameters<
    +    typeof shouldSuppressMessagingToolReplies
    +  >[0]["messagingToolSentTargets"];
    +  originatingTo?: string;
    +  accountId?: string;
    +}): { replyPayloads: ReplyPayload[]; didLogHeartbeatStrip: boolean } {
    +  let didLogHeartbeatStrip = params.didLogHeartbeatStrip;
    +  const sanitizedPayloads = params.isHeartbeat
    +    ? params.payloads
    +    : params.payloads.flatMap((payload) => {
    +        let text = payload.text;
    +
    +        if (payload.isError && text && isBunFetchSocketError(text)) {
    +          text = formatBunFetchSocketError(text);
    +        }
    +
    +        if (!text || !text.includes("HEARTBEAT_OK")) {
    +          return [{ ...payload, text }];
    +        }
    +        const stripped = stripHeartbeatToken(text, { mode: "message" });
    +        if (stripped.didStrip && !didLogHeartbeatStrip) {
    +          didLogHeartbeatStrip = true;
    +          logVerbose("Stripped stray HEARTBEAT_OK token from reply");
    +        }
    +        const hasMedia =
    +          Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
    +        if (stripped.shouldSkip && !hasMedia) return [];
    +        return [{ ...payload, text: stripped.text }];
    +      });
    +
    +  const replyTaggedPayloads: ReplyPayload[] = applyReplyThreading({
    +    payloads: sanitizedPayloads,
    +    replyToMode: params.replyToMode,
    +    replyToChannel: params.replyToChannel,
    +    currentMessageId: params.currentMessageId,
    +  })
    +    .map((payload) => {
    +      const parsed = parseReplyDirectives(payload.text ?? "", {
    +        currentMessageId: params.currentMessageId,
    +        silentToken: SILENT_REPLY_TOKEN,
    +      });
    +      const mediaUrls = payload.mediaUrls ?? parsed.mediaUrls;
    +      const mediaUrl = payload.mediaUrl ?? parsed.mediaUrl ?? mediaUrls?.[0];
    +      return {
    +        ...payload,
    +        text: parsed.text ? parsed.text : undefined,
    +        mediaUrls,
    +        mediaUrl,
    +        replyToId: payload.replyToId ?? parsed.replyToId,
    +        replyToTag: payload.replyToTag || parsed.replyToTag,
    +        replyToCurrent: payload.replyToCurrent || parsed.replyToCurrent,
    +        audioAsVoice: Boolean(payload.audioAsVoice || parsed.audioAsVoice),
    +      };
    +    })
    +    .filter(isRenderablePayload);
    +
    +  // Drop final payloads only when block streaming succeeded end-to-end.
    +  // If streaming aborted (e.g., timeout), fall back to final payloads.
    +  const shouldDropFinalPayloads =
    +    params.blockStreamingEnabled &&
    +    Boolean(params.blockReplyPipeline?.didStream()) &&
    +    !params.blockReplyPipeline?.isAborted();
    +  const messagingToolSentTexts = params.messagingToolSentTexts ?? [];
    +  const messagingToolSentTargets = params.messagingToolSentTargets ?? [];
    +  const suppressMessagingToolReplies = shouldSuppressMessagingToolReplies({
    +    messageProvider: params.messageProvider,
    +    messagingToolSentTargets,
    +    originatingTo: params.originatingTo,
    +    accountId: params.accountId,
    +  });
    +  const dedupedPayloads = filterMessagingToolDuplicates({
    +    payloads: replyTaggedPayloads,
    +    sentTexts: messagingToolSentTexts,
    +  });
    +  const filteredPayloads = shouldDropFinalPayloads
    +    ? []
    +    : params.blockStreamingEnabled
    +      ? dedupedPayloads.filter(
    +          (payload) => !params.blockReplyPipeline?.hasSentPayload(payload),
    +        )
    +      : dedupedPayloads;
    +  const replyPayloads = suppressMessagingToolReplies ? [] : filteredPayloads;
    +
    +  return {
    +    replyPayloads,
    +    didLogHeartbeatStrip,
    +  };
    +}
    
  • src/auto-reply/reply/agent-runner.ts+85 762 modified
    @@ -1,32 +1,12 @@
     import crypto from "node:crypto";
    -import fs from "node:fs";
    -import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
    -import { runCliAgent } from "../../agents/cli-runner.js";
    -import { getCliSessionId, setCliSessionId } from "../../agents/cli-session.js";
    +import { setCliSessionId } from "../../agents/cli-session.js";
     import { lookupContextTokens } from "../../agents/context.js";
     import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
     import { resolveModelAuthMode } from "../../agents/model-auth.js";
    -import { runWithModelFallback } from "../../agents/model-fallback.js";
     import { isCliProvider } from "../../agents/model-selection.js";
    +import { queueEmbeddedPiMessage } from "../../agents/pi-embedded.js";
    +import { hasNonzeroUsage } from "../../agents/usage.js";
     import {
    -  queueEmbeddedPiMessage,
    -  runEmbeddedPiAgent,
    -} from "../../agents/pi-embedded.js";
    -import {
    -  isCompactionFailureError,
    -  isContextOverflowError,
    -} from "../../agents/pi-embedded-helpers.js";
    -import {
    -  resolveSandboxConfigForAgent,
    -  resolveSandboxRuntimeStatus,
    -} from "../../agents/sandbox.js";
    -import { hasNonzeroUsage, type NormalizedUsage } from "../../agents/usage.js";
    -import { getChannelDock } from "../../channels/dock.js";
    -import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
    -import { normalizeChannelId } from "../../channels/registry.js";
    -import type { ClawdbotConfig } from "../../config/config.js";
    -import {
    -  loadSessionStore,
       resolveAgentIdFromSessionKey,
       resolveSessionTranscriptPath,
       type SessionEntry,
    @@ -35,49 +15,35 @@ import {
     } from "../../config/sessions.js";
     import type { TypingMode } from "../../config/types.js";
     import { logVerbose } from "../../globals.js";
    -import {
    -  emitAgentEvent,
    -  registerAgentRunContext,
    -} from "../../infra/agent-events.js";
    -import { isAudioFileName } from "../../media/mime.js";
     import { defaultRuntime } from "../../runtime.js";
    -import { isReasoningTagProvider } from "../../utils/provider-utils.js";
    -import {
    -  estimateUsageCost,
    -  formatTokenCount,
    -  formatUsd,
    -  resolveModelCostConfig,
    -} from "../../utils/usage-format.js";
    -import { stripHeartbeatToken } from "../heartbeat.js";
    +import { resolveModelCostConfig } from "../../utils/usage-format.js";
     import type { OriginatingChannelType, TemplateContext } from "../templating.js";
    -import { normalizeVerboseLevel, type VerboseLevel } from "../thinking.js";
    -import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
    +import type { VerboseLevel } from "../thinking.js";
     import type { GetReplyOptions, ReplyPayload } from "../types.js";
    +import { runAgentTurnWithFallback } from "./agent-runner-execution.js";
    +import {
    +  createShouldEmitToolResult,
    +  finalizeWithFollowup,
    +  isAudioPayload,
    +  signalTypingIfNeeded,
    +} from "./agent-runner-helpers.js";
    +import { runMemoryFlushIfNeeded } from "./agent-runner-memory.js";
    +import { buildReplyPayloads } from "./agent-runner-payloads.js";
    +import {
    +  appendUsageLine,
    +  formatResponseUsageLine,
    +} from "./agent-runner-utils.js";
     import {
       createAudioAsVoiceBuffer,
       createBlockReplyPipeline,
     } from "./block-reply-pipeline.js";
     import { resolveBlockStreamingCoalescing } from "./block-streaming.js";
     import { createFollowupRunner } from "./followup-runner.js";
    -import {
    -  resolveMemoryFlushContextWindowTokens,
    -  resolveMemoryFlushSettings,
    -  shouldRunMemoryFlush,
    -} from "./memory-flush.js";
     import {
       enqueueFollowupRun,
       type FollowupRun,
       type QueueSettings,
    -  scheduleFollowupDrain,
     } from "./queue.js";
    -import { parseReplyDirectives } from "./reply-directives.js";
    -import {
    -  applyReplyTagsToPayload,
    -  applyReplyThreading,
    -  filterMessagingToolDuplicates,
    -  isRenderablePayload,
    -  shouldSuppressMessagingToolReplies,
    -} from "./reply-payloads.js";
     import {
       createReplyToModeFilterForChannel,
       resolveReplyToMode,
    @@ -86,113 +52,8 @@ import { incrementCompactionCount } from "./session-updates.js";
     import type { TypingController } from "./typing.js";
     import { createTypingSignaler } from "./typing-mode.js";
     
    -const BUN_FETCH_SOCKET_ERROR_RE = /socket connection was closed unexpectedly/i;
     const BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000;
     
    -/**
    - * Build provider-specific threading context for tool auto-injection.
    - */
    -function buildThreadingToolContext(params: {
    -  sessionCtx: TemplateContext;
    -  config: ClawdbotConfig | undefined;
    -  hasRepliedRef: { value: boolean } | undefined;
    -}): ChannelThreadingToolContext {
    -  const { sessionCtx, config, hasRepliedRef } = params;
    -  if (!config) return {};
    -  const provider = normalizeChannelId(sessionCtx.Provider);
    -  if (!provider) return {};
    -  const dock = getChannelDock(provider);
    -  if (!dock?.threading?.buildToolContext) return {};
    -  return (
    -    dock.threading.buildToolContext({
    -      cfg: config,
    -      accountId: sessionCtx.AccountId,
    -      context: {
    -        Channel: sessionCtx.Provider,
    -        To: sessionCtx.To,
    -        ReplyToId: sessionCtx.ReplyToId,
    -        ThreadLabel: sessionCtx.ThreadLabel,
    -      },
    -      hasRepliedRef,
    -    }) ?? {}
    -  );
    -}
    -
    -const isBunFetchSocketError = (message?: string) =>
    -  Boolean(message && BUN_FETCH_SOCKET_ERROR_RE.test(message));
    -
    -const formatBunFetchSocketError = (message: string) => {
    -  const trimmed = message.trim();
    -  return [
    -    "⚠️ LLM connection failed. This could be due to server issues, network problems, or context length exceeded (e.g., with local LLMs like LM Studio). Original error:",
    -    "```",
    -    trimmed || "Unknown error",
    -    "```",
    -  ].join("\n");
    -};
    -
    -const formatResponseUsageLine = (params: {
    -  usage?: NormalizedUsage;
    -  showCost: boolean;
    -  costConfig?: {
    -    input: number;
    -    output: number;
    -    cacheRead: number;
    -    cacheWrite: number;
    -  };
    -}): string | null => {
    -  const usage = params.usage;
    -  if (!usage) return null;
    -  const input = usage.input;
    -  const output = usage.output;
    -  if (typeof input !== "number" && typeof output !== "number") return null;
    -  const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
    -  const outputLabel =
    -    typeof output === "number" ? formatTokenCount(output) : "?";
    -  const cost =
    -    params.showCost && typeof input === "number" && typeof output === "number"
    -      ? estimateUsageCost({
    -          usage: {
    -            input,
    -            output,
    -            cacheRead: usage.cacheRead,
    -            cacheWrite: usage.cacheWrite,
    -          },
    -          cost: params.costConfig,
    -        })
    -      : undefined;
    -  const costLabel = params.showCost ? formatUsd(cost) : undefined;
    -  const suffix = costLabel ? ` · est ${costLabel}` : "";
    -  return `Usage: ${inputLabel} in / ${outputLabel} out${suffix}`;
    -};
    -
    -const appendUsageLine = (
    -  payloads: ReplyPayload[],
    -  line: string,
    -): ReplyPayload[] => {
    -  let index = -1;
    -  for (let i = payloads.length - 1; i >= 0; i -= 1) {
    -    if (payloads[i]?.text) {
    -      index = i;
    -      break;
    -    }
    -  }
    -  if (index === -1) return [...payloads, { text: line }];
    -  const existing = payloads[index];
    -  const existingText = existing.text ?? "";
    -  const separator = existingText.endsWith("\n") ? "" : "\n";
    -  const next = {
    -    ...existing,
    -    text: `${existingText}${separator}${line}`,
    -  };
    -  const updated = payloads.slice();
    -  updated[index] = next;
    -  return updated;
    -};
    -
    -const resolveEnforceFinalTag = (run: FollowupRun["run"], provider: string) =>
    -  Boolean(run.enforceFinalTag || isReasoningTagProvider(provider));
    -
     export async function runReplyAgent(params: {
       commandBody: string;
       followupRun: FollowupRun;
    @@ -261,31 +122,16 @@ export async function runReplyAgent(params: {
         isHeartbeat,
       });
     
    -  const shouldEmitToolResult = () => {
    -    if (!sessionKey || !storePath) {
    -      return resolvedVerboseLevel === "on";
    -    }
    -    try {
    -      const store = loadSessionStore(storePath);
    -      const entry = store[sessionKey];
    -      const current = normalizeVerboseLevel(entry?.verboseLevel);
    -      if (current) return current === "on";
    -    } catch {
    -      // ignore store read failures
    -    }
    -    return resolvedVerboseLevel === "on";
    -  };
    +  const shouldEmitToolResult = createShouldEmitToolResult({
    +    sessionKey,
    +    storePath,
    +    resolvedVerboseLevel,
    +  });
     
       const pendingToolTasks = new Set<Promise<void>>();
       const blockReplyTimeoutMs =
         opts?.blockReplyTimeoutMs ?? BLOCK_REPLY_SEND_TIMEOUT_MS;
     
    -  const hasAudioMedia = (urls?: string[]): boolean =>
    -    Boolean(urls?.some((u) => isAudioFileName(u)));
    -  const isAudioPayload = (payload: ReplyPayload) =>
    -    hasAudioMedia(
    -      payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined),
    -    );
       const replyToChannel =
         sessionCtx.OriginatingChannel ??
         ((sessionCtx.Surface ?? sessionCtx.Provider)?.toLowerCase() as
    @@ -351,133 +197,20 @@ export async function runReplyAgent(params: {
         return undefined;
       }
     
    -  const memoryFlushSettings = resolveMemoryFlushSettings(cfg);
    -  const memoryFlushWritable = (() => {
    -    if (!sessionKey) return true;
    -    const runtime = resolveSandboxRuntimeStatus({ cfg, sessionKey });
    -    if (!runtime.sandboxed) return true;
    -    const sandboxCfg = resolveSandboxConfigForAgent(cfg, runtime.agentId);
    -    return sandboxCfg.workspaceAccess === "rw";
    -  })();
    -  const shouldFlushMemory =
    -    memoryFlushSettings &&
    -    memoryFlushWritable &&
    -    !isHeartbeat &&
    -    !isCliProvider(followupRun.run.provider, cfg) &&
    -    shouldRunMemoryFlush({
    -      entry:
    -        activeSessionEntry ??
    -        (sessionKey ? activeSessionStore?.[sessionKey] : undefined),
    -      contextWindowTokens: resolveMemoryFlushContextWindowTokens({
    -        modelId: followupRun.run.model ?? defaultModel,
    -        agentCfgContextTokens,
    -      }),
    -      reserveTokensFloor: memoryFlushSettings.reserveTokensFloor,
    -      softThresholdTokens: memoryFlushSettings.softThresholdTokens,
    -    });
    -  if (shouldFlushMemory) {
    -    const flushRunId = crypto.randomUUID();
    -    if (sessionKey) {
    -      registerAgentRunContext(flushRunId, {
    -        sessionKey,
    -        verboseLevel: resolvedVerboseLevel,
    -      });
    -    }
    -    let memoryCompactionCompleted = false;
    -    const flushSystemPrompt = [
    -      followupRun.run.extraSystemPrompt,
    -      memoryFlushSettings.systemPrompt,
    -    ]
    -      .filter(Boolean)
    -      .join("\n\n");
    -    try {
    -      await runWithModelFallback({
    -        cfg: followupRun.run.config,
    -        provider: followupRun.run.provider,
    -        model: followupRun.run.model,
    -        fallbacksOverride: resolveAgentModelFallbacksOverride(
    -          followupRun.run.config,
    -          resolveAgentIdFromSessionKey(followupRun.run.sessionKey),
    -        ),
    -        run: (provider, model) =>
    -          runEmbeddedPiAgent({
    -            sessionId: followupRun.run.sessionId,
    -            sessionKey,
    -            messageProvider:
    -              sessionCtx.Provider?.trim().toLowerCase() || undefined,
    -            agentAccountId: sessionCtx.AccountId,
    -            // Provider threading context for tool auto-injection
    -            ...buildThreadingToolContext({
    -              sessionCtx,
    -              config: followupRun.run.config,
    -              hasRepliedRef: opts?.hasRepliedRef,
    -            }),
    -            sessionFile: followupRun.run.sessionFile,
    -            workspaceDir: followupRun.run.workspaceDir,
    -            agentDir: followupRun.run.agentDir,
    -            config: followupRun.run.config,
    -            skillsSnapshot: followupRun.run.skillsSnapshot,
    -            prompt: memoryFlushSettings.prompt,
    -            extraSystemPrompt: flushSystemPrompt,
    -            ownerNumbers: followupRun.run.ownerNumbers,
    -            enforceFinalTag: resolveEnforceFinalTag(followupRun.run, provider),
    -            provider,
    -            model,
    -            authProfileId: followupRun.run.authProfileId,
    -            thinkLevel: followupRun.run.thinkLevel,
    -            verboseLevel: followupRun.run.verboseLevel,
    -            reasoningLevel: followupRun.run.reasoningLevel,
    -            bashElevated: followupRun.run.bashElevated,
    -            timeoutMs: followupRun.run.timeoutMs,
    -            runId: flushRunId,
    -            onAgentEvent: (evt) => {
    -              if (evt.stream === "compaction") {
    -                const phase =
    -                  typeof evt.data.phase === "string" ? evt.data.phase : "";
    -                const willRetry = Boolean(evt.data.willRetry);
    -                if (phase === "end" && !willRetry) {
    -                  memoryCompactionCompleted = true;
    -                }
    -              }
    -            },
    -          }),
    -      });
    -      let memoryFlushCompactionCount =
    -        activeSessionEntry?.compactionCount ??
    -        (sessionKey ? activeSessionStore?.[sessionKey]?.compactionCount : 0) ??
    -        0;
    -      if (memoryCompactionCompleted) {
    -        const nextCount = await incrementCompactionCount({
    -          sessionEntry: activeSessionEntry,
    -          sessionStore: activeSessionStore,
    -          sessionKey,
    -          storePath,
    -        });
    -        if (typeof nextCount === "number") {
    -          memoryFlushCompactionCount = nextCount;
    -        }
    -      }
    -      if (storePath && sessionKey) {
    -        try {
    -          const updatedEntry = await updateSessionStoreEntry({
    -            storePath,
    -            sessionKey,
    -            update: async () => ({
    -              memoryFlushAt: Date.now(),
    -              memoryFlushCompactionCount,
    -            }),
    -          });
    -          if (updatedEntry) {
    -            activeSessionEntry = updatedEntry;
    -          }
    -        } catch (err) {
    -          logVerbose(`failed to persist memory flush metadata: ${String(err)}`);
    -        }
    -      }
    -    } catch (err) {
    -      logVerbose(`memory flush run failed: ${String(err)}`);
    -    }
    -  }
    +  activeSessionEntry = await runMemoryFlushIfNeeded({
    +    cfg,
    +    followupRun,
    +    sessionCtx,
    +    opts,
    +    defaultModel,
    +    agentCfgContextTokens,
    +    resolvedVerboseLevel,
    +    sessionEntry: activeSessionEntry,
    +    sessionStore: activeSessionStore,
    +    sessionKey,
    +    storePath,
    +    isHeartbeat,
    +  });
     
       const runFollowupTurn = createFollowupRunner({
         opts,
    @@ -491,13 +224,6 @@ export async function runReplyAgent(params: {
         agentCfgContextTokens,
       });
     
    -  const finalizeWithFollowup = <T>(value: T): T => {
    -    scheduleFollowupDrain(queueKey, runFollowupTurn);
    -    return value;
    -  };
    -
    -  let didLogHeartbeatStrip = false;
    -  let autoCompactionCompleted = false;
       let responseUsageLine: string | undefined;
       const resetSessionAfterCompactionFailure = async (
         reason: string,
    @@ -540,380 +266,39 @@ export async function runReplyAgent(params: {
         return true;
       };
       try {
    -    const runId = crypto.randomUUID();
    -    if (sessionKey) {
    -      registerAgentRunContext(runId, {
    -        sessionKey,
    -        verboseLevel: resolvedVerboseLevel,
    -      });
    -    }
    -    let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
    -    let fallbackProvider = followupRun.run.provider;
    -    let fallbackModel = followupRun.run.model;
    -    let didResetAfterCompactionFailure = false;
    -    while (true) {
    -      try {
    -        const allowPartialStream = !(
    -          followupRun.run.reasoningLevel === "stream" && opts?.onReasoningStream
    -        );
    -        const normalizeStreamingText = (
    -          payload: ReplyPayload,
    -        ): { text?: string; skip: boolean } => {
    -          if (!allowPartialStream) return { skip: true };
    -          let text = payload.text;
    -          if (!isHeartbeat && text?.includes("HEARTBEAT_OK")) {
    -            const stripped = stripHeartbeatToken(text, {
    -              mode: "message",
    -            });
    -            if (stripped.didStrip && !didLogHeartbeatStrip) {
    -              didLogHeartbeatStrip = true;
    -              logVerbose("Stripped stray HEARTBEAT_OK token from reply");
    -            }
    -            if (stripped.shouldSkip && (payload.mediaUrls?.length ?? 0) === 0) {
    -              return { skip: true };
    -            }
    -            text = stripped.text;
    -          }
    -          if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
    -            return { skip: true };
    -          }
    -          return { text, skip: false };
    -        };
    -        const handlePartialForTyping = async (
    -          payload: ReplyPayload,
    -        ): Promise<string | undefined> => {
    -          const { text, skip } = normalizeStreamingText(payload);
    -          if (skip || !text) return undefined;
    -          await typingSignals.signalTextDelta(text);
    -          return text;
    -        };
    -        const fallbackResult = await runWithModelFallback({
    -          cfg: followupRun.run.config,
    -          provider: followupRun.run.provider,
    -          model: followupRun.run.model,
    -          fallbacksOverride: resolveAgentModelFallbacksOverride(
    -            followupRun.run.config,
    -            resolveAgentIdFromSessionKey(followupRun.run.sessionKey),
    -          ),
    -          run: (provider, model) => {
    -            if (isCliProvider(provider, followupRun.run.config)) {
    -              const startedAt = Date.now();
    -              emitAgentEvent({
    -                runId,
    -                stream: "lifecycle",
    -                data: {
    -                  phase: "start",
    -                  startedAt,
    -                },
    -              });
    -              const cliSessionId = getCliSessionId(
    -                activeSessionEntry,
    -                provider,
    -              );
    -              return runCliAgent({
    -                sessionId: followupRun.run.sessionId,
    -                sessionKey,
    -                sessionFile: followupRun.run.sessionFile,
    -                workspaceDir: followupRun.run.workspaceDir,
    -                config: followupRun.run.config,
    -                prompt: commandBody,
    -                provider,
    -                model,
    -                thinkLevel: followupRun.run.thinkLevel,
    -                timeoutMs: followupRun.run.timeoutMs,
    -                runId,
    -                extraSystemPrompt: followupRun.run.extraSystemPrompt,
    -                ownerNumbers: followupRun.run.ownerNumbers,
    -                cliSessionId,
    -              })
    -                .then((result) => {
    -                  emitAgentEvent({
    -                    runId,
    -                    stream: "lifecycle",
    -                    data: {
    -                      phase: "end",
    -                      startedAt,
    -                      endedAt: Date.now(),
    -                    },
    -                  });
    -                  return result;
    -                })
    -                .catch((err) => {
    -                  emitAgentEvent({
    -                    runId,
    -                    stream: "lifecycle",
    -                    data: {
    -                      phase: "error",
    -                      startedAt,
    -                      endedAt: Date.now(),
    -                      error: err instanceof Error ? err.message : String(err),
    -                    },
    -                  });
    -                  throw err;
    -                });
    -            }
    -            return runEmbeddedPiAgent({
    -              sessionId: followupRun.run.sessionId,
    -              sessionKey,
    -              messageProvider:
    -                sessionCtx.Provider?.trim().toLowerCase() || undefined,
    -              agentAccountId: sessionCtx.AccountId,
    -              // Provider threading context for tool auto-injection
    -              ...buildThreadingToolContext({
    -                sessionCtx,
    -                config: followupRun.run.config,
    -                hasRepliedRef: opts?.hasRepliedRef,
    -              }),
    -              sessionFile: followupRun.run.sessionFile,
    -              workspaceDir: followupRun.run.workspaceDir,
    -              agentDir: followupRun.run.agentDir,
    -              config: followupRun.run.config,
    -              skillsSnapshot: followupRun.run.skillsSnapshot,
    -              prompt: commandBody,
    -              extraSystemPrompt: followupRun.run.extraSystemPrompt,
    -              ownerNumbers: followupRun.run.ownerNumbers,
    -              enforceFinalTag: resolveEnforceFinalTag(
    -                followupRun.run,
    -                provider,
    -              ),
    -              provider,
    -              model,
    -              authProfileId: followupRun.run.authProfileId,
    -              thinkLevel: followupRun.run.thinkLevel,
    -              verboseLevel: followupRun.run.verboseLevel,
    -              reasoningLevel: followupRun.run.reasoningLevel,
    -              bashElevated: followupRun.run.bashElevated,
    -              timeoutMs: followupRun.run.timeoutMs,
    -              runId,
    -              blockReplyBreak: resolvedBlockStreamingBreak,
    -              blockReplyChunking,
    -              onPartialReply: allowPartialStream
    -                ? async (payload) => {
    -                    const textForTyping = await handlePartialForTyping(payload);
    -                    if (!opts?.onPartialReply || textForTyping === undefined)
    -                      return;
    -                    await opts.onPartialReply({
    -                      text: textForTyping,
    -                      mediaUrls: payload.mediaUrls,
    -                    });
    -                  }
    -                : undefined,
    -              onAssistantMessageStart: async () => {
    -                await typingSignals.signalMessageStart();
    -              },
    -              onReasoningStream:
    -                typingSignals.shouldStartOnReasoning || opts?.onReasoningStream
    -                  ? async (payload) => {
    -                      await typingSignals.signalReasoningDelta();
    -                      await opts?.onReasoningStream?.({
    -                        text: payload.text,
    -                        mediaUrls: payload.mediaUrls,
    -                      });
    -                    }
    -                  : undefined,
    -              onAgentEvent: (evt) => {
    -                // Trigger typing when tools start executing
    -                if (evt.stream === "tool") {
    -                  const phase =
    -                    typeof evt.data.phase === "string" ? evt.data.phase : "";
    -                  if (phase === "start" || phase === "update") {
    -                    void typingSignals.signalToolStart();
    -                  }
    -                }
    -                // Track auto-compaction completion
    -                if (evt.stream === "compaction") {
    -                  const phase =
    -                    typeof evt.data.phase === "string" ? evt.data.phase : "";
    -                  const willRetry = Boolean(evt.data.willRetry);
    -                  if (phase === "end" && !willRetry) {
    -                    autoCompactionCompleted = true;
    -                  }
    -                }
    -              },
    -              onBlockReply:
    -                blockStreamingEnabled && opts?.onBlockReply
    -                  ? async (payload) => {
    -                      const { text, skip } = normalizeStreamingText(payload);
    -                      const hasPayloadMedia =
    -                        (payload.mediaUrls?.length ?? 0) > 0;
    -                      if (skip && !hasPayloadMedia) return;
    -                      const taggedPayload = applyReplyTagsToPayload(
    -                        {
    -                          text,
    -                          mediaUrls: payload.mediaUrls,
    -                          mediaUrl: payload.mediaUrls?.[0],
    -                        },
    -                        sessionCtx.MessageSid,
    -                      );
    -                      // Let through payloads with audioAsVoice flag even if empty (need to track it)
    -                      if (
    -                        !isRenderablePayload(taggedPayload) &&
    -                        !payload.audioAsVoice
    -                      )
    -                        return;
    -                      const parsed = parseReplyDirectives(
    -                        taggedPayload.text ?? "",
    -                        {
    -                          currentMessageId: sessionCtx.MessageSid,
    -                          silentToken: SILENT_REPLY_TOKEN,
    -                        },
    -                      );
    -                      const cleaned = parsed.text || undefined;
    -                      const hasRenderableMedia =
    -                        Boolean(taggedPayload.mediaUrl) ||
    -                        (taggedPayload.mediaUrls?.length ?? 0) > 0;
    -                      // Skip empty payloads unless they have audioAsVoice flag (need to track it)
    -                      if (
    -                        !cleaned &&
    -                        !hasRenderableMedia &&
    -                        !payload.audioAsVoice &&
    -                        !parsed.audioAsVoice
    -                      )
    -                        return;
    -                      if (parsed.isSilent && !hasRenderableMedia) return;
    -
    -                      const blockPayload: ReplyPayload = applyReplyToMode({
    -                        ...taggedPayload,
    -                        text: cleaned,
    -                        audioAsVoice: Boolean(
    -                          parsed.audioAsVoice || payload.audioAsVoice,
    -                        ),
    -                        replyToId: taggedPayload.replyToId ?? parsed.replyToId,
    -                        replyToTag:
    -                          taggedPayload.replyToTag || parsed.replyToTag,
    -                        replyToCurrent:
    -                          taggedPayload.replyToCurrent || parsed.replyToCurrent,
    -                      });
    -
    -                      void typingSignals
    -                        .signalTextDelta(cleaned ?? taggedPayload.text)
    -                        .catch((err) => {
    -                          logVerbose(
    -                            `block reply typing signal failed: ${String(err)}`,
    -                          );
    -                        });
    -
    -                      blockReplyPipeline?.enqueue(blockPayload);
    -                    }
    -                  : undefined,
    -              onBlockReplyFlush:
    -                blockStreamingEnabled && blockReplyPipeline
    -                  ? async () => {
    -                      await blockReplyPipeline.flush({ force: true });
    -                    }
    -                  : undefined,
    -              shouldEmitToolResult,
    -              onToolResult: opts?.onToolResult
    -                ? (payload) => {
    -                    // `subscribeEmbeddedPiSession` may invoke tool callbacks without awaiting them.
    -                    // If a tool callback starts typing after the run finalized, we can end up with
    -                    // a typing loop that never sees a matching markRunComplete(). Track and drain.
    -                    const task = (async () => {
    -                      const { text, skip } = normalizeStreamingText(payload);
    -                      if (skip) return;
    -                      await typingSignals.signalTextDelta(text);
    -                      await opts.onToolResult?.({
    -                        text,
    -                        mediaUrls: payload.mediaUrls,
    -                      });
    -                    })()
    -                      .catch((err) => {
    -                        logVerbose(
    -                          `tool result delivery failed: ${String(err)}`,
    -                        );
    -                      })
    -                      .finally(() => {
    -                        pendingToolTasks.delete(task);
    -                      });
    -                    pendingToolTasks.add(task);
    -                  }
    -                : undefined,
    -            });
    -          },
    -        });
    -        runResult = fallbackResult.result;
    -        fallbackProvider = fallbackResult.provider;
    -        fallbackModel = fallbackResult.model;
    -
    -        // Some embedded runs surface context overflow as an error payload instead of throwing.
    -        // Treat those as a session-level failure and auto-recover by starting a fresh session.
    -        const embeddedError = runResult.meta?.error;
    -        if (
    -          embeddedError &&
    -          isContextOverflowError(embeddedError.message) &&
    -          !didResetAfterCompactionFailure &&
    -          (await resetSessionAfterCompactionFailure(embeddedError.message))
    -        ) {
    -          didResetAfterCompactionFailure = true;
    -          continue;
    -        }
    -
    -        break;
    -      } catch (err) {
    -        const message = err instanceof Error ? err.message : String(err);
    -        const isContextOverflow =
    -          isContextOverflowError(message) ||
    -          /context.*overflow|too large|context window/i.test(message);
    -        const isCompactionFailure = isCompactionFailureError(message);
    -        const isSessionCorruption =
    -          /function call turn comes immediately after/i.test(message);
    -
    -        if (
    -          isCompactionFailure &&
    -          !didResetAfterCompactionFailure &&
    -          (await resetSessionAfterCompactionFailure(message))
    -        ) {
    -          didResetAfterCompactionFailure = true;
    -          continue;
    -        }
    -
    -        // Auto-recover from Gemini session corruption by resetting the session
    -        if (
    -          isSessionCorruption &&
    -          sessionKey &&
    -          activeSessionStore &&
    -          storePath
    -        ) {
    -          const corruptedSessionId = activeSessionEntry?.sessionId;
    -          defaultRuntime.error(
    -            `Session history corrupted (Gemini function call ordering). Resetting session: ${sessionKey}`,
    -          );
    -
    -          try {
    -            // Delete transcript file if it exists
    -            if (corruptedSessionId) {
    -              const transcriptPath =
    -                resolveSessionTranscriptPath(corruptedSessionId);
    -              try {
    -                fs.unlinkSync(transcriptPath);
    -              } catch {
    -                // Ignore if file doesn't exist
    -              }
    -            }
    -
    -            // Remove session entry from store
    -            delete activeSessionStore[sessionKey];
    -            await saveSessionStore(storePath, activeSessionStore);
    -          } catch (cleanupErr) {
    -            defaultRuntime.error(
    -              `Failed to reset corrupted session ${sessionKey}: ${String(cleanupErr)}`,
    -            );
    -          }
    -
    -          return finalizeWithFollowup({
    -            text: "⚠️ Session history was corrupted. I've reset the conversation - please try again!",
    -          });
    -        }
    +    const runOutcome = await runAgentTurnWithFallback({
    +      commandBody,
    +      followupRun,
    +      sessionCtx,
    +      opts,
    +      typingSignals,
    +      blockReplyPipeline,
    +      blockStreamingEnabled,
    +      blockReplyChunking,
    +      resolvedBlockStreamingBreak,
    +      applyReplyToMode,
    +      shouldEmitToolResult,
    +      pendingToolTasks,
    +      resetSessionAfterCompactionFailure,
    +      isHeartbeat,
    +      sessionKey,
    +      getActiveSessionEntry: () => activeSessionEntry,
    +      activeSessionStore,
    +      storePath,
    +      resolvedVerboseLevel,
    +    });
     
    -        defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
    -        return finalizeWithFollowup({
    -          text: isContextOverflow
    -            ? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
    -            : `⚠️ Agent failed before reply: ${message}. Check gateway logs for details.`,
    -        });
    -      }
    +    if (runOutcome.kind === "final") {
    +      return finalizeWithFollowup(
    +        runOutcome.payload,
    +        queueKey,
    +        runFollowupTurn,
    +      );
         }
     
    +    const { runResult, fallbackProvider, fallbackModel } = runOutcome;
    +    let { didLogHeartbeatStrip, autoCompactionCompleted } = runOutcome;
    +
         if (
           shouldInjectGroupIntro &&
           activeSessionEntry &&
    @@ -942,95 +327,31 @@ export async function runReplyAgent(params: {
         // Drain any late tool/block deliveries before deciding there's "nothing to send".
         // Otherwise, a late typing trigger (e.g. from a tool callback) can outlive the run and
         // keep the typing indicator stuck.
    -    if (payloadArray.length === 0) return finalizeWithFollowup(undefined);
    -
    -    const sanitizedPayloads = isHeartbeat
    -      ? payloadArray
    -      : payloadArray.flatMap((payload) => {
    -          let text = payload.text;
    -
    -          if (payload.isError && text && isBunFetchSocketError(text)) {
    -            text = formatBunFetchSocketError(text);
    -          }
    -
    -          if (!text || !text.includes("HEARTBEAT_OK"))
    -            return [{ ...payload, text }];
    -          const stripped = stripHeartbeatToken(text, { mode: "message" });
    -          if (stripped.didStrip && !didLogHeartbeatStrip) {
    -            didLogHeartbeatStrip = true;
    -            logVerbose("Stripped stray HEARTBEAT_OK token from reply");
    -          }
    -          const hasMedia =
    -            Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
    -          if (stripped.shouldSkip && !hasMedia) return [];
    -          return [{ ...payload, text: stripped.text }];
    -        });
    -
    -    const replyTaggedPayloads: ReplyPayload[] = applyReplyThreading({
    -      payloads: sanitizedPayloads,
    +    if (payloadArray.length === 0)
    +      return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);
    +
    +    const payloadResult = buildReplyPayloads({
    +      payloads: payloadArray,
    +      isHeartbeat,
    +      didLogHeartbeatStrip,
    +      blockStreamingEnabled,
    +      blockReplyPipeline,
           replyToMode,
           replyToChannel,
           currentMessageId: sessionCtx.MessageSid,
    -    })
    -      .map((payload) => {
    -        const parsed = parseReplyDirectives(payload.text ?? "", {
    -          currentMessageId: sessionCtx.MessageSid,
    -          silentToken: SILENT_REPLY_TOKEN,
    -        });
    -        const mediaUrls = payload.mediaUrls ?? parsed.mediaUrls;
    -        const mediaUrl = payload.mediaUrl ?? parsed.mediaUrl ?? mediaUrls?.[0];
    -        return {
    -          ...payload,
    -          text: parsed.text ? parsed.text : undefined,
    -          mediaUrls,
    -          mediaUrl,
    -          replyToId: payload.replyToId ?? parsed.replyToId,
    -          replyToTag: payload.replyToTag || parsed.replyToTag,
    -          replyToCurrent: payload.replyToCurrent || parsed.replyToCurrent,
    -          audioAsVoice: Boolean(payload.audioAsVoice || parsed.audioAsVoice),
    -        };
    -      })
    -      .filter(isRenderablePayload);
    -
    -    // Drop final payloads only when block streaming succeeded end-to-end.
    -    // If streaming aborted (e.g., timeout), fall back to final payloads.
    -    const shouldDropFinalPayloads =
    -      blockStreamingEnabled &&
    -      Boolean(blockReplyPipeline?.didStream()) &&
    -      !blockReplyPipeline?.isAborted();
    -    const messagingToolSentTexts = runResult.messagingToolSentTexts ?? [];
    -    const messagingToolSentTargets = runResult.messagingToolSentTargets ?? [];
    -    const suppressMessagingToolReplies = shouldSuppressMessagingToolReplies({
           messageProvider: followupRun.run.messageProvider,
    -      messagingToolSentTargets,
    +      messagingToolSentTexts: runResult.messagingToolSentTexts,
    +      messagingToolSentTargets: runResult.messagingToolSentTargets,
           originatingTo: sessionCtx.OriginatingTo ?? sessionCtx.To,
           accountId: sessionCtx.AccountId,
         });
    -    const dedupedPayloads = filterMessagingToolDuplicates({
    -      payloads: replyTaggedPayloads,
    -      sentTexts: messagingToolSentTexts,
    -    });
    -    const filteredPayloads = shouldDropFinalPayloads
    -      ? []
    -      : blockStreamingEnabled
    -        ? dedupedPayloads.filter(
    -            (payload) => !blockReplyPipeline?.hasSentPayload(payload),
    -          )
    -        : dedupedPayloads;
    -    const replyPayloads = suppressMessagingToolReplies ? [] : filteredPayloads;
    +    const { replyPayloads } = payloadResult;
    +    didLogHeartbeatStrip = payloadResult.didLogHeartbeatStrip;
     
    -    if (replyPayloads.length === 0) return finalizeWithFollowup(undefined);
    +    if (replyPayloads.length === 0)
    +      return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);
     
    -    const shouldSignalTyping = replyPayloads.some((payload) => {
    -      const trimmed = payload.text?.trim();
    -      if (trimmed) return true;
    -      if (payload.mediaUrl) return true;
    -      if (payload.mediaUrls && payload.mediaUrls.length > 0) return true;
    -      return false;
    -    });
    -    if (shouldSignalTyping) {
    -      await typingSignals.signalRunStart();
    -    }
    +    await signalTypingIfNeeded(replyPayloads, typingSignals);
     
         const usage = runResult.meta.agentMeta?.usage;
         const modelUsed =
    @@ -1166,6 +487,8 @@ export async function runReplyAgent(params: {
     
         return finalizeWithFollowup(
           finalPayloads.length === 1 ? finalPayloads[0] : finalPayloads,
    +      queueKey,
    +      runFollowupTurn,
         );
       } finally {
         blockReplyPipeline?.stop();
    
  • src/auto-reply/reply/agent-runner-utils.ts+122 0 added
    @@ -0,0 +1,122 @@
    +import type { NormalizedUsage } from "../../agents/usage.js";
    +import { getChannelDock } from "../../channels/dock.js";
    +import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
    +import { normalizeChannelId } from "../../channels/registry.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import { isReasoningTagProvider } from "../../utils/provider-utils.js";
    +import {
    +  estimateUsageCost,
    +  formatTokenCount,
    +  formatUsd,
    +} from "../../utils/usage-format.js";
    +import type { TemplateContext } from "../templating.js";
    +import type { ReplyPayload } from "../types.js";
    +import type { FollowupRun } from "./queue.js";
    +
    +const BUN_FETCH_SOCKET_ERROR_RE = /socket connection was closed unexpectedly/i;
    +
    +/**
    + * Build provider-specific threading context for tool auto-injection.
    + */
    +export function buildThreadingToolContext(params: {
    +  sessionCtx: TemplateContext;
    +  config: ClawdbotConfig | undefined;
    +  hasRepliedRef: { value: boolean } | undefined;
    +}): ChannelThreadingToolContext {
    +  const { sessionCtx, config, hasRepliedRef } = params;
    +  if (!config) return {};
    +  const provider = normalizeChannelId(sessionCtx.Provider);
    +  if (!provider) return {};
    +  const dock = getChannelDock(provider);
    +  if (!dock?.threading?.buildToolContext) return {};
    +  return (
    +    dock.threading.buildToolContext({
    +      cfg: config,
    +      accountId: sessionCtx.AccountId,
    +      context: {
    +        Channel: sessionCtx.Provider,
    +        To: sessionCtx.To,
    +        ReplyToId: sessionCtx.ReplyToId,
    +        ThreadLabel: sessionCtx.ThreadLabel,
    +      },
    +      hasRepliedRef,
    +    }) ?? {}
    +  );
    +}
    +
    +export const isBunFetchSocketError = (message?: string) =>
    +  Boolean(message && BUN_FETCH_SOCKET_ERROR_RE.test(message));
    +
    +export const formatBunFetchSocketError = (message: string) => {
    +  const trimmed = message.trim();
    +  return [
    +    "⚠️ LLM connection failed. This could be due to server issues, network problems, or context length exceeded (e.g., with local LLMs like LM Studio). Original error:",
    +    "```",
    +    trimmed || "Unknown error",
    +    "```",
    +  ].join("\n");
    +};
    +
    +export const formatResponseUsageLine = (params: {
    +  usage?: NormalizedUsage;
    +  showCost: boolean;
    +  costConfig?: {
    +    input: number;
    +    output: number;
    +    cacheRead: number;
    +    cacheWrite: number;
    +  };
    +}): string | null => {
    +  const usage = params.usage;
    +  if (!usage) return null;
    +  const input = usage.input;
    +  const output = usage.output;
    +  if (typeof input !== "number" && typeof output !== "number") return null;
    +  const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
    +  const outputLabel =
    +    typeof output === "number" ? formatTokenCount(output) : "?";
    +  const cost =
    +    params.showCost && typeof input === "number" && typeof output === "number"
    +      ? estimateUsageCost({
    +          usage: {
    +            input,
    +            output,
    +            cacheRead: usage.cacheRead,
    +            cacheWrite: usage.cacheWrite,
    +          },
    +          cost: params.costConfig,
    +        })
    +      : undefined;
    +  const costLabel = params.showCost ? formatUsd(cost) : undefined;
    +  const suffix = costLabel ? ` · est ${costLabel}` : "";
    +  return `Usage: ${inputLabel} in / ${outputLabel} out${suffix}`;
    +};
    +
    +export const appendUsageLine = (
    +  payloads: ReplyPayload[],
    +  line: string,
    +): ReplyPayload[] => {
    +  let index = -1;
    +  for (let i = payloads.length - 1; i >= 0; i -= 1) {
    +    if (payloads[i]?.text) {
    +      index = i;
    +      break;
    +    }
    +  }
    +  if (index === -1) return [...payloads, { text: line }];
    +  const existing = payloads[index];
    +  const existingText = existing.text ?? "";
    +  const separator = existingText.endsWith("\n") ? "" : "\n";
    +  const next = {
    +    ...existing,
    +    text: `${existingText}${separator}${line}`,
    +  };
    +  const updated = payloads.slice();
    +  updated[index] = next;
    +  return updated;
    +};
    +
    +export const resolveEnforceFinalTag = (
    +  run: FollowupRun["run"],
    +  provider: string,
    +) => Boolean(run.enforceFinalTag || isReasoningTagProvider(provider));
    
  • src/auto-reply/reply/commands-bash.ts+36 0 added
    @@ -0,0 +1,36 @@
    +import { logVerbose } from "../../globals.js";
    +import { handleBashChatCommand } from "./bash-command.js";
    +import type { CommandHandler } from "./commands-types.js";
    +
    +export const handleBashCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  const { command } = params;
    +  const bashSlashRequested =
    +    command.commandBodyNormalized === "/bash" ||
    +    command.commandBodyNormalized.startsWith("/bash ");
    +  const bashBangRequested = command.commandBodyNormalized.startsWith("!");
    +  if (
    +    !bashSlashRequested &&
    +    !(bashBangRequested && command.isAuthorizedSender)
    +  ) {
    +    return null;
    +  }
    +  if (!command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /bash from unauthorized sender: ${command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  const reply = await handleBashChatCommand({
    +    ctx: params.ctx,
    +    cfg: params.cfg,
    +    agentId: params.agentId,
    +    sessionKey: params.sessionKey,
    +    isGroup: params.isGroup,
    +    elevated: params.elevated,
    +  });
    +  return { shouldContinue: false, reply };
    +};
    
  • src/auto-reply/reply/commands-compact.ts+119 0 added
    @@ -0,0 +1,119 @@
    +import {
    +  abortEmbeddedPiRun,
    +  compactEmbeddedPiSession,
    +  isEmbeddedPiRunActive,
    +  waitForEmbeddedPiRunEnd,
    +} from "../../agents/pi-embedded.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import { resolveSessionFilePath } from "../../config/sessions.js";
    +import { logVerbose } from "../../globals.js";
    +import { enqueueSystemEvent } from "../../infra/system-events.js";
    +import { formatContextUsageShort, formatTokenCount } from "../status.js";
    +import type { CommandHandler } from "./commands-types.js";
    +import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
    +import { incrementCompactionCount } from "./session-updates.js";
    +
    +function extractCompactInstructions(params: {
    +  rawBody?: string;
    +  ctx: import("../templating.js").MsgContext;
    +  cfg: ClawdbotConfig;
    +  agentId?: string;
    +  isGroup: boolean;
    +}): string | undefined {
    +  const raw = stripStructuralPrefixes(params.rawBody ?? "");
    +  const stripped = params.isGroup
    +    ? stripMentions(raw, params.ctx, params.cfg, params.agentId)
    +    : raw;
    +  const trimmed = stripped.trim();
    +  if (!trimmed) return undefined;
    +  const lowered = trimmed.toLowerCase();
    +  const prefix = lowered.startsWith("/compact") ? "/compact" : null;
    +  if (!prefix) return undefined;
    +  let rest = trimmed.slice(prefix.length).trimStart();
    +  if (rest.startsWith(":")) rest = rest.slice(1).trimStart();
    +  return rest.length ? rest : undefined;
    +}
    +
    +export const handleCompactCommand: CommandHandler = async (params) => {
    +  const compactRequested =
    +    params.command.commandBodyNormalized === "/compact" ||
    +    params.command.commandBodyNormalized.startsWith("/compact ");
    +  if (!compactRequested) return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /compact from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  if (!params.sessionEntry?.sessionId) {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: "⚙️ Compaction unavailable (missing session id)." },
    +    };
    +  }
    +  const sessionId = params.sessionEntry.sessionId;
    +  if (isEmbeddedPiRunActive(sessionId)) {
    +    abortEmbeddedPiRun(sessionId);
    +    await waitForEmbeddedPiRunEnd(sessionId, 15_000);
    +  }
    +  const customInstructions = extractCompactInstructions({
    +    rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body,
    +    ctx: params.ctx,
    +    cfg: params.cfg,
    +    agentId: params.agentId,
    +    isGroup: params.isGroup,
    +  });
    +  const result = await compactEmbeddedPiSession({
    +    sessionId,
    +    sessionKey: params.sessionKey,
    +    messageChannel: params.command.channel,
    +    sessionFile: resolveSessionFilePath(sessionId, params.sessionEntry),
    +    workspaceDir: params.workspaceDir,
    +    config: params.cfg,
    +    skillsSnapshot: params.sessionEntry.skillsSnapshot,
    +    provider: params.provider,
    +    model: params.model,
    +    thinkLevel:
    +      params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()),
    +    bashElevated: {
    +      enabled: false,
    +      allowed: false,
    +      defaultLevel: "off",
    +    },
    +    customInstructions,
    +    ownerNumbers:
    +      params.command.ownerList.length > 0
    +        ? params.command.ownerList
    +        : undefined,
    +  });
    +
    +  const totalTokens =
    +    params.sessionEntry.totalTokens ??
    +    (params.sessionEntry.inputTokens ?? 0) +
    +      (params.sessionEntry.outputTokens ?? 0);
    +  const contextSummary = formatContextUsageShort(
    +    totalTokens > 0 ? totalTokens : null,
    +    params.contextTokens ?? params.sessionEntry.contextTokens ?? null,
    +  );
    +  const compactLabel = result.ok
    +    ? result.compacted
    +      ? result.result?.tokensBefore
    +        ? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
    +        : "Compacted"
    +      : "Compaction skipped"
    +    : "Compaction failed";
    +  if (result.ok && result.compacted) {
    +    await incrementCompactionCount({
    +      sessionEntry: params.sessionEntry,
    +      sessionStore: params.sessionStore,
    +      sessionKey: params.sessionKey,
    +      storePath: params.storePath,
    +    });
    +  }
    +  const reason = result.reason?.trim();
    +  const line = reason
    +    ? `${compactLabel}: ${reason} • ${contextSummary}`
    +    : `${compactLabel} • ${contextSummary}`;
    +  enqueueSystemEvent(line, { sessionKey: params.sessionKey });
    +  return { shouldContinue: false, reply: { text: `⚙️ ${line}` } };
    +};
    
  • src/auto-reply/reply/commands-config.ts+255 0 added
    @@ -0,0 +1,255 @@
    +import {
    +  readConfigFileSnapshot,
    +  validateConfigObject,
    +  writeConfigFile,
    +} from "../../config/config.js";
    +import {
    +  getConfigValueAtPath,
    +  parseConfigPath,
    +  setConfigValueAtPath,
    +  unsetConfigValueAtPath,
    +} from "../../config/config-paths.js";
    +import {
    +  getConfigOverrides,
    +  resetConfigOverrides,
    +  setConfigOverride,
    +  unsetConfigOverride,
    +} from "../../config/runtime-overrides.js";
    +import { logVerbose } from "../../globals.js";
    +import type { CommandHandler } from "./commands-types.js";
    +import { parseConfigCommand } from "./config-commands.js";
    +import { parseDebugCommand } from "./debug-commands.js";
    +
    +export const handleConfigCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  const configCommand = parseConfigCommand(
    +    params.command.commandBodyNormalized,
    +  );
    +  if (!configCommand) return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /config from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  if (params.cfg.commands?.config !== true) {
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: "⚠️ /config is disabled. Set commands.config=true to enable.",
    +      },
    +    };
    +  }
    +  if (configCommand.action === "error") {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: `⚠️ ${configCommand.message}` },
    +    };
    +  }
    +  const snapshot = await readConfigFileSnapshot();
    +  if (
    +    !snapshot.valid ||
    +    !snapshot.parsed ||
    +    typeof snapshot.parsed !== "object"
    +  ) {
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: "⚠️ Config file is invalid; fix it before using /config.",
    +      },
    +    };
    +  }
    +  const parsedBase = structuredClone(
    +    snapshot.parsed as Record<string, unknown>,
    +  );
    +
    +  if (configCommand.action === "show") {
    +    const pathRaw = configCommand.path?.trim();
    +    if (pathRaw) {
    +      const parsedPath = parseConfigPath(pathRaw);
    +      if (!parsedPath.ok || !parsedPath.path) {
    +        return {
    +          shouldContinue: false,
    +          reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
    +        };
    +      }
    +      const value = getConfigValueAtPath(parsedBase, parsedPath.path);
    +      const rendered = JSON.stringify(value ?? null, null, 2);
    +      return {
    +        shouldContinue: false,
    +        reply: {
    +          text: `⚙️ Config ${pathRaw}:\n\`\`\`json\n${rendered}\n\`\`\``,
    +        },
    +      };
    +    }
    +    const json = JSON.stringify(parsedBase, null, 2);
    +    return {
    +      shouldContinue: false,
    +      reply: { text: `⚙️ Config (raw):\n\`\`\`json\n${json}\n\`\`\`` },
    +    };
    +  }
    +
    +  if (configCommand.action === "unset") {
    +    const parsedPath = parseConfigPath(configCommand.path);
    +    if (!parsedPath.ok || !parsedPath.path) {
    +      return {
    +        shouldContinue: false,
    +        reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
    +      };
    +    }
    +    const removed = unsetConfigValueAtPath(parsedBase, parsedPath.path);
    +    if (!removed) {
    +      return {
    +        shouldContinue: false,
    +        reply: { text: `⚙️ No config value found for ${configCommand.path}.` },
    +      };
    +    }
    +    const validated = validateConfigObject(parsedBase);
    +    if (!validated.ok) {
    +      const issue = validated.issues[0];
    +      return {
    +        shouldContinue: false,
    +        reply: {
    +          text: `⚠️ Config invalid after unset (${issue.path}: ${issue.message}).`,
    +        },
    +      };
    +    }
    +    await writeConfigFile(validated.config);
    +    return {
    +      shouldContinue: false,
    +      reply: { text: `⚙️ Config updated: ${configCommand.path} removed.` },
    +    };
    +  }
    +
    +  if (configCommand.action === "set") {
    +    const parsedPath = parseConfigPath(configCommand.path);
    +    if (!parsedPath.ok || !parsedPath.path) {
    +      return {
    +        shouldContinue: false,
    +        reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
    +      };
    +    }
    +    setConfigValueAtPath(parsedBase, parsedPath.path, configCommand.value);
    +    const validated = validateConfigObject(parsedBase);
    +    if (!validated.ok) {
    +      const issue = validated.issues[0];
    +      return {
    +        shouldContinue: false,
    +        reply: {
    +          text: `⚠️ Config invalid after set (${issue.path}: ${issue.message}).`,
    +        },
    +      };
    +    }
    +    await writeConfigFile(validated.config);
    +    const valueLabel =
    +      typeof configCommand.value === "string"
    +        ? `"${configCommand.value}"`
    +        : JSON.stringify(configCommand.value);
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: `⚙️ Config updated: ${configCommand.path}=${valueLabel ?? "null"}`,
    +      },
    +    };
    +  }
    +
    +  return null;
    +};
    +
    +export const handleDebugCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  const debugCommand = parseDebugCommand(params.command.commandBodyNormalized);
    +  if (!debugCommand) return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /debug from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  if (params.cfg.commands?.debug !== true) {
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: "⚠️ /debug is disabled. Set commands.debug=true to enable.",
    +      },
    +    };
    +  }
    +  if (debugCommand.action === "error") {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: `⚠️ ${debugCommand.message}` },
    +    };
    +  }
    +  if (debugCommand.action === "show") {
    +    const overrides = getConfigOverrides();
    +    const hasOverrides = Object.keys(overrides).length > 0;
    +    if (!hasOverrides) {
    +      return {
    +        shouldContinue: false,
    +        reply: { text: "⚙️ Debug overrides: (none)" },
    +      };
    +    }
    +    const json = JSON.stringify(overrides, null, 2);
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: `⚙️ Debug overrides (memory-only):\n\`\`\`json\n${json}\n\`\`\``,
    +      },
    +    };
    +  }
    +  if (debugCommand.action === "reset") {
    +    resetConfigOverrides();
    +    return {
    +      shouldContinue: false,
    +      reply: { text: "⚙️ Debug overrides cleared; using config on disk." },
    +    };
    +  }
    +  if (debugCommand.action === "unset") {
    +    const result = unsetConfigOverride(debugCommand.path);
    +    if (!result.ok) {
    +      return {
    +        shouldContinue: false,
    +        reply: { text: `⚠️ ${result.error ?? "Invalid path."}` },
    +      };
    +    }
    +    if (!result.removed) {
    +      return {
    +        shouldContinue: false,
    +        reply: {
    +          text: `⚙️ No debug override found for ${debugCommand.path}.`,
    +        },
    +      };
    +    }
    +    return {
    +      shouldContinue: false,
    +      reply: { text: `⚙️ Debug override removed for ${debugCommand.path}.` },
    +    };
    +  }
    +  if (debugCommand.action === "set") {
    +    const result = setConfigOverride(debugCommand.path, debugCommand.value);
    +    if (!result.ok) {
    +      return {
    +        shouldContinue: false,
    +        reply: { text: `⚠️ ${result.error ?? "Invalid override."}` },
    +      };
    +    }
    +    const valueLabel =
    +      typeof debugCommand.value === "string"
    +        ? `"${debugCommand.value}"`
    +        : JSON.stringify(debugCommand.value);
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: `⚙️ Debug override set: ${debugCommand.path}=${valueLabel ?? "null"}`,
    +      },
    +    };
    +  }
    +
    +  return null;
    +};
    
  • src/auto-reply/reply/commands-context.ts+48 0 added
    @@ -0,0 +1,48 @@
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import { resolveCommandAuthorization } from "../command-auth.js";
    +import { normalizeCommandBody } from "../commands-registry.js";
    +import type { MsgContext } from "../templating.js";
    +import type { CommandContext } from "./commands-types.js";
    +import { stripMentions } from "./mentions.js";
    +
    +export function buildCommandContext(params: {
    +  ctx: MsgContext;
    +  cfg: ClawdbotConfig;
    +  agentId?: string;
    +  sessionKey?: string;
    +  isGroup: boolean;
    +  triggerBodyNormalized: string;
    +  commandAuthorized: boolean;
    +}): CommandContext {
    +  const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } =
    +    params;
    +  const auth = resolveCommandAuthorization({
    +    ctx,
    +    cfg,
    +    commandAuthorized: params.commandAuthorized,
    +  });
    +  const surface = (ctx.Surface ?? ctx.Provider ?? "").trim().toLowerCase();
    +  const channel = (ctx.Provider ?? surface).trim().toLowerCase();
    +  const abortKey =
    +    sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
    +  const rawBodyNormalized = triggerBodyNormalized;
    +  const commandBodyNormalized = normalizeCommandBody(
    +    isGroup
    +      ? stripMentions(rawBodyNormalized, ctx, cfg, agentId)
    +      : rawBodyNormalized,
    +  );
    +
    +  return {
    +    surface,
    +    channel,
    +    channelId: auth.providerId,
    +    ownerList: auth.ownerList,
    +    isAuthorizedSender: auth.isAuthorizedSender,
    +    senderId: auth.senderId,
    +    abortKey,
    +    rawBodyNormalized,
    +    commandBodyNormalized,
    +    from: auth.from,
    +    to: auth.to,
    +  };
    +}
    
  • src/auto-reply/reply/commands-core.ts+81 0 added
    @@ -0,0 +1,81 @@
    +import { logVerbose } from "../../globals.js";
    +import { resolveSendPolicy } from "../../sessions/send-policy.js";
    +import { shouldHandleTextCommands } from "../commands-registry.js";
    +import { handleBashCommand } from "./commands-bash.js";
    +import { handleCompactCommand } from "./commands-compact.js";
    +import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
    +import {
    +  handleCommandsListCommand,
    +  handleHelpCommand,
    +  handleStatusCommand,
    +  handleWhoamiCommand,
    +} from "./commands-info.js";
    +import {
    +  handleAbortTrigger,
    +  handleActivationCommand,
    +  handleRestartCommand,
    +  handleSendPolicyCommand,
    +  handleStopCommand,
    +} from "./commands-session.js";
    +import type {
    +  CommandHandler,
    +  CommandHandlerResult,
    +  HandleCommandsParams,
    +} from "./commands-types.js";
    +
    +const HANDLERS: CommandHandler[] = [
    +  handleBashCommand,
    +  handleActivationCommand,
    +  handleSendPolicyCommand,
    +  handleRestartCommand,
    +  handleHelpCommand,
    +  handleCommandsListCommand,
    +  handleStatusCommand,
    +  handleWhoamiCommand,
    +  handleConfigCommand,
    +  handleDebugCommand,
    +  handleStopCommand,
    +  handleCompactCommand,
    +  handleAbortTrigger,
    +];
    +
    +export async function handleCommands(
    +  params: HandleCommandsParams,
    +): Promise<CommandHandlerResult> {
    +  const resetRequested =
    +    params.command.commandBodyNormalized === "/reset" ||
    +    params.command.commandBodyNormalized === "/new";
    +  if (resetRequested && !params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /reset from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +
    +  const allowTextCommands = shouldHandleTextCommands({
    +    cfg: params.cfg,
    +    surface: params.command.surface,
    +    commandSource: params.ctx.CommandSource,
    +  });
    +
    +  for (const handler of HANDLERS) {
    +    const result = await handler(params, allowTextCommands);
    +    if (result) return result;
    +  }
    +
    +  const sendPolicy = resolveSendPolicy({
    +    cfg: params.cfg,
    +    entry: params.sessionEntry,
    +    sessionKey: params.sessionKey,
    +    channel: params.sessionEntry?.channel ?? params.command.channel,
    +    chatType: params.sessionEntry?.chatType,
    +  });
    +  if (sendPolicy === "deny") {
    +    logVerbose(
    +      `Send blocked by policy for session ${params.sessionKey ?? "unknown"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +
    +  return { shouldContinue: true };
    +}
    
  • src/auto-reply/reply/commands-info.ts+109 0 added
    @@ -0,0 +1,109 @@
    +import { logVerbose } from "../../globals.js";
    +import { buildCommandsMessage, buildHelpMessage } from "../status.js";
    +import { buildStatusReply } from "./commands-status.js";
    +import type { CommandHandler } from "./commands-types.js";
    +
    +export const handleHelpCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  if (params.command.commandBodyNormalized !== "/help") return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /help from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  return {
    +    shouldContinue: false,
    +    reply: { text: buildHelpMessage(params.cfg) },
    +  };
    +};
    +
    +export const handleCommandsListCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  if (params.command.commandBodyNormalized !== "/commands") return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /commands from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  return {
    +    shouldContinue: false,
    +    reply: { text: buildCommandsMessage(params.cfg) },
    +  };
    +};
    +
    +export const handleStatusCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  const statusRequested =
    +    params.directives.hasStatusDirective ||
    +    params.command.commandBodyNormalized === "/status";
    +  if (!statusRequested) return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /status from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  const reply = await buildStatusReply({
    +    cfg: params.cfg,
    +    command: params.command,
    +    sessionEntry: params.sessionEntry,
    +    sessionKey: params.sessionKey,
    +    sessionScope: params.sessionScope,
    +    provider: params.provider,
    +    model: params.model,
    +    contextTokens: params.contextTokens,
    +    resolvedThinkLevel: params.resolvedThinkLevel,
    +    resolvedVerboseLevel: params.resolvedVerboseLevel,
    +    resolvedReasoningLevel: params.resolvedReasoningLevel,
    +    resolvedElevatedLevel: params.resolvedElevatedLevel,
    +    resolveDefaultThinkingLevel: params.resolveDefaultThinkingLevel,
    +    isGroup: params.isGroup,
    +    defaultGroupActivation: params.defaultGroupActivation,
    +  });
    +  return { shouldContinue: false, reply };
    +};
    +
    +export const handleWhoamiCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  if (params.command.commandBodyNormalized !== "/whoami") return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /whoami from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  const senderId = params.ctx.SenderId ?? "";
    +  const senderUsername = params.ctx.SenderUsername ?? "";
    +  const lines = ["🧭 Identity", `Channel: ${params.command.channel}`];
    +  if (senderId) lines.push(`User id: ${senderId}`);
    +  if (senderUsername) {
    +    const handle = senderUsername.startsWith("@")
    +      ? senderUsername
    +      : `@${senderUsername}`;
    +    lines.push(`Username: ${handle}`);
    +  }
    +  if (params.ctx.ChatType === "group" && params.ctx.From) {
    +    lines.push(`Chat: ${params.ctx.From}`);
    +  }
    +  if (params.ctx.MessageThreadId != null) {
    +    lines.push(`Thread: ${params.ctx.MessageThreadId}`);
    +  }
    +  if (senderId) {
    +    lines.push(`AllowFrom: ${senderId}`);
    +  }
    +  return { shouldContinue: false, reply: { text: lines.join("\n") } };
    +};
    
  • src/auto-reply/reply/commands-session.ts+252 0 added
    @@ -0,0 +1,252 @@
    +import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
    +import type { SessionEntry } from "../../config/sessions.js";
    +import { saveSessionStore } from "../../config/sessions.js";
    +import { logVerbose } from "../../globals.js";
    +import {
    +  scheduleGatewaySigusr1Restart,
    +  triggerClawdbotRestart,
    +} from "../../infra/restart.js";
    +import { parseAgentSessionKey } from "../../routing/session-key.js";
    +import { parseActivationCommand } from "../group-activation.js";
    +import { parseSendPolicyCommand } from "../send-policy.js";
    +import { isAbortTrigger, setAbortMemory } from "./abort.js";
    +import type { CommandHandler } from "./commands-types.js";
    +
    +function resolveSessionEntryForKey(
    +  store: Record<string, SessionEntry> | undefined,
    +  sessionKey: string | undefined,
    +) {
    +  if (!store || !sessionKey) return {};
    +  const direct = store[sessionKey];
    +  if (direct) return { entry: direct, key: sessionKey };
    +  const parsed = parseAgentSessionKey(sessionKey);
    +  const legacyKey = parsed?.rest;
    +  if (legacyKey && store[legacyKey]) {
    +    return { entry: store[legacyKey], key: legacyKey };
    +  }
    +  return {};
    +}
    +
    +function resolveAbortTarget(params: {
    +  ctx: { CommandTargetSessionKey?: string | null };
    +  sessionKey?: string;
    +  sessionEntry?: SessionEntry;
    +  sessionStore?: Record<string, SessionEntry>;
    +}) {
    +  const targetSessionKey =
    +    params.ctx.CommandTargetSessionKey?.trim() || params.sessionKey;
    +  const { entry, key } = resolveSessionEntryForKey(
    +    params.sessionStore,
    +    targetSessionKey,
    +  );
    +  if (entry && key) return { entry, key, sessionId: entry.sessionId };
    +  if (params.sessionEntry && params.sessionKey) {
    +    return {
    +      entry: params.sessionEntry,
    +      key: params.sessionKey,
    +      sessionId: params.sessionEntry.sessionId,
    +    };
    +  }
    +  return { entry: undefined, key: targetSessionKey, sessionId: undefined };
    +}
    +
    +export const handleActivationCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  const activationCommand = parseActivationCommand(
    +    params.command.commandBodyNormalized,
    +  );
    +  if (!activationCommand.hasCommand) return null;
    +  if (!params.isGroup) {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: "⚙️ Group activation only applies to group chats." },
    +    };
    +  }
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /activation from unauthorized sender in group: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  if (!activationCommand.mode) {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: "⚙️ Usage: /activation mention|always" },
    +    };
    +  }
    +  if (params.sessionEntry && params.sessionStore && params.sessionKey) {
    +    params.sessionEntry.groupActivation = activationCommand.mode;
    +    params.sessionEntry.groupActivationNeedsSystemIntro = true;
    +    params.sessionEntry.updatedAt = Date.now();
    +    params.sessionStore[params.sessionKey] = params.sessionEntry;
    +    if (params.storePath) {
    +      await saveSessionStore(params.storePath, params.sessionStore);
    +    }
    +  }
    +  return {
    +    shouldContinue: false,
    +    reply: {
    +      text: `⚙️ Group activation set to ${activationCommand.mode}.`,
    +    },
    +  };
    +};
    +
    +export const handleSendPolicyCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  const sendPolicyCommand = parseSendPolicyCommand(
    +    params.command.commandBodyNormalized,
    +  );
    +  if (!sendPolicyCommand.hasCommand) return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /send from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  if (!sendPolicyCommand.mode) {
    +    return {
    +      shouldContinue: false,
    +      reply: { text: "⚙️ Usage: /send on|off|inherit" },
    +    };
    +  }
    +  if (params.sessionEntry && params.sessionStore && params.sessionKey) {
    +    if (sendPolicyCommand.mode === "inherit") {
    +      delete params.sessionEntry.sendPolicy;
    +    } else {
    +      params.sessionEntry.sendPolicy = sendPolicyCommand.mode;
    +    }
    +    params.sessionEntry.updatedAt = Date.now();
    +    params.sessionStore[params.sessionKey] = params.sessionEntry;
    +    if (params.storePath) {
    +      await saveSessionStore(params.storePath, params.sessionStore);
    +    }
    +  }
    +  const label =
    +    sendPolicyCommand.mode === "inherit"
    +      ? "inherit"
    +      : sendPolicyCommand.mode === "allow"
    +        ? "on"
    +        : "off";
    +  return {
    +    shouldContinue: false,
    +    reply: { text: `⚙️ Send policy set to ${label}.` },
    +  };
    +};
    +
    +export const handleRestartCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  if (params.command.commandBodyNormalized !== "/restart") return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /restart from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  if (params.cfg.commands?.restart !== true) {
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: "⚠️ /restart is disabled. Set commands.restart=true to enable.",
    +      },
    +    };
    +  }
    +  const hasSigusr1Listener = process.listenerCount("SIGUSR1") > 0;
    +  if (hasSigusr1Listener) {
    +    scheduleGatewaySigusr1Restart({ reason: "/restart" });
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: "⚙️ Restarting clawdbot in-process (SIGUSR1); back in a few seconds.",
    +      },
    +    };
    +  }
    +  const restartMethod = triggerClawdbotRestart();
    +  if (!restartMethod.ok) {
    +    const detail = restartMethod.detail
    +      ? ` Details: ${restartMethod.detail}`
    +      : "";
    +    return {
    +      shouldContinue: false,
    +      reply: {
    +        text: `⚠️ Restart failed (${restartMethod.method}).${detail}`,
    +      },
    +    };
    +  }
    +  return {
    +    shouldContinue: false,
    +    reply: {
    +      text: `⚙️ Restarting clawdbot via ${restartMethod.method}; give me a few seconds to come back online.`,
    +    },
    +  };
    +};
    +
    +export const handleStopCommand: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  if (params.command.commandBodyNormalized !== "/stop") return null;
    +  if (!params.command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /stop from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
    +    );
    +    return { shouldContinue: false };
    +  }
    +  const abortTarget = resolveAbortTarget({
    +    ctx: params.ctx,
    +    sessionKey: params.sessionKey,
    +    sessionEntry: params.sessionEntry,
    +    sessionStore: params.sessionStore,
    +  });
    +  if (abortTarget.sessionId) {
    +    abortEmbeddedPiRun(abortTarget.sessionId);
    +  }
    +  if (abortTarget.entry && params.sessionStore && abortTarget.key) {
    +    abortTarget.entry.abortedLastRun = true;
    +    abortTarget.entry.updatedAt = Date.now();
    +    params.sessionStore[abortTarget.key] = abortTarget.entry;
    +    if (params.storePath) {
    +      await saveSessionStore(params.storePath, params.sessionStore);
    +    }
    +  } else if (params.command.abortKey) {
    +    setAbortMemory(params.command.abortKey, true);
    +  }
    +  return { shouldContinue: false, reply: { text: "⚙️ Agent was aborted." } };
    +};
    +
    +export const handleAbortTrigger: CommandHandler = async (
    +  params,
    +  allowTextCommands,
    +) => {
    +  if (!allowTextCommands) return null;
    +  if (!isAbortTrigger(params.command.rawBodyNormalized)) return null;
    +  const abortTarget = resolveAbortTarget({
    +    ctx: params.ctx,
    +    sessionKey: params.sessionKey,
    +    sessionEntry: params.sessionEntry,
    +    sessionStore: params.sessionStore,
    +  });
    +  if (abortTarget.sessionId) {
    +    abortEmbeddedPiRun(abortTarget.sessionId);
    +  }
    +  if (abortTarget.entry && params.sessionStore && abortTarget.key) {
    +    abortTarget.entry.abortedLastRun = true;
    +    abortTarget.entry.updatedAt = Date.now();
    +    params.sessionStore[abortTarget.key] = abortTarget.entry;
    +    if (params.storePath) {
    +      await saveSessionStore(params.storePath, params.sessionStore);
    +    }
    +  } else if (params.command.abortKey) {
    +    setAbortMemory(params.command.abortKey, true);
    +  }
    +  return { shouldContinue: false, reply: { text: "⚙️ Agent was aborted." } };
    +};
    
  • src/auto-reply/reply/commands-status.ts+223 0 added
    @@ -0,0 +1,223 @@
    +import {
    +  resolveAgentDir,
    +  resolveDefaultAgentId,
    +  resolveSessionAgentId,
    +} from "../../agents/agent-scope.js";
    +import {
    +  ensureAuthProfileStore,
    +  resolveAuthProfileDisplayLabel,
    +  resolveAuthProfileOrder,
    +} from "../../agents/auth-profiles.js";
    +import {
    +  getCustomProviderApiKey,
    +  resolveEnvApiKey,
    +} from "../../agents/model-auth.js";
    +import { normalizeProviderId } from "../../agents/model-selection.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import type { SessionEntry, SessionScope } from "../../config/sessions.js";
    +import { logVerbose } from "../../globals.js";
    +import {
    +  formatUsageSummaryLine,
    +  loadProviderUsageSummary,
    +  resolveUsageProviderId,
    +} from "../../infra/provider-usage.js";
    +import { normalizeGroupActivation } from "../group-activation.js";
    +import { buildStatusMessage } from "../status.js";
    +import type {
    +  ElevatedLevel,
    +  ReasoningLevel,
    +  ThinkLevel,
    +  VerboseLevel,
    +} from "../thinking.js";
    +import type { ReplyPayload } from "../types.js";
    +import type { CommandContext } from "./commands-types.js";
    +import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js";
    +
    +function formatApiKeySnippet(apiKey: string): string {
    +  const compact = apiKey.replace(/\s+/g, "");
    +  if (!compact) return "unknown";
    +  const edge = compact.length >= 12 ? 6 : 4;
    +  const head = compact.slice(0, edge);
    +  const tail = compact.slice(-edge);
    +  return `${head}…${tail}`;
    +}
    +
    +function resolveModelAuthLabel(
    +  provider?: string,
    +  cfg?: ClawdbotConfig,
    +  sessionEntry?: SessionEntry,
    +  agentDir?: string,
    +): string | undefined {
    +  const resolved = provider?.trim();
    +  if (!resolved) return undefined;
    +
    +  const providerKey = normalizeProviderId(resolved);
    +  const store = ensureAuthProfileStore(agentDir, {
    +    allowKeychainPrompt: false,
    +  });
    +  const profileOverride = sessionEntry?.authProfileOverride?.trim();
    +  const order = resolveAuthProfileOrder({
    +    cfg,
    +    store,
    +    provider: providerKey,
    +    preferredProfile: profileOverride,
    +  });
    +  const candidates = [profileOverride, ...order].filter(Boolean) as string[];
    +
    +  for (const profileId of candidates) {
    +    const profile = store.profiles[profileId];
    +    if (!profile || normalizeProviderId(profile.provider) !== providerKey) {
    +      continue;
    +    }
    +    const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
    +    if (profile.type === "oauth") {
    +      return `oauth${label ? ` (${label})` : ""}`;
    +    }
    +    if (profile.type === "token") {
    +      const snippet = formatApiKeySnippet(profile.token);
    +      return `token ${snippet}${label ? ` (${label})` : ""}`;
    +    }
    +    const snippet = formatApiKeySnippet(profile.key);
    +    return `api-key ${snippet}${label ? ` (${label})` : ""}`;
    +  }
    +
    +  const envKey = resolveEnvApiKey(providerKey);
    +  if (envKey?.apiKey) {
    +    if (envKey.source.includes("OAUTH_TOKEN")) {
    +      return `oauth (${envKey.source})`;
    +    }
    +    return `api-key ${formatApiKeySnippet(envKey.apiKey)} (${envKey.source})`;
    +  }
    +
    +  const customKey = getCustomProviderApiKey(cfg, providerKey);
    +  if (customKey) {
    +    return `api-key ${formatApiKeySnippet(customKey)} (models.json)`;
    +  }
    +
    +  return "unknown";
    +}
    +
    +export async function buildStatusReply(params: {
    +  cfg: ClawdbotConfig;
    +  command: CommandContext;
    +  sessionEntry?: SessionEntry;
    +  sessionKey: string;
    +  sessionScope?: SessionScope;
    +  provider: string;
    +  model: string;
    +  contextTokens: number;
    +  resolvedThinkLevel?: ThinkLevel;
    +  resolvedVerboseLevel: VerboseLevel;
    +  resolvedReasoningLevel: ReasoningLevel;
    +  resolvedElevatedLevel?: ElevatedLevel;
    +  resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
    +  isGroup: boolean;
    +  defaultGroupActivation: () => "always" | "mention";
    +}): Promise<ReplyPayload | undefined> {
    +  const {
    +    cfg,
    +    command,
    +    sessionEntry,
    +    sessionKey,
    +    sessionScope,
    +    provider,
    +    model,
    +    contextTokens,
    +    resolvedThinkLevel,
    +    resolvedVerboseLevel,
    +    resolvedReasoningLevel,
    +    resolvedElevatedLevel,
    +    resolveDefaultThinkingLevel,
    +    isGroup,
    +    defaultGroupActivation,
    +  } = params;
    +  if (!command.isAuthorizedSender) {
    +    logVerbose(
    +      `Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`,
    +    );
    +    return undefined;
    +  }
    +  const statusAgentId = sessionKey
    +    ? resolveSessionAgentId({ sessionKey, config: cfg })
    +    : resolveDefaultAgentId(cfg);
    +  const statusAgentDir = resolveAgentDir(cfg, statusAgentId);
    +  let usageLine: string | null = null;
    +  try {
    +    const usageProvider = resolveUsageProviderId(provider);
    +    if (usageProvider) {
    +      const usageSummary = await loadProviderUsageSummary({
    +        timeoutMs: 3500,
    +        providers: [usageProvider],
    +        agentDir: statusAgentDir,
    +      });
    +      usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() });
    +      if (
    +        !usageLine &&
    +        (resolvedVerboseLevel === "on" || resolvedElevatedLevel === "on")
    +      ) {
    +        const entry = usageSummary.providers[0];
    +        if (entry?.error) {
    +          usageLine = `📊 Usage: ${entry.displayName} (${entry.error})`;
    +        }
    +      }
    +    }
    +  } catch {
    +    usageLine = null;
    +  }
    +  const queueSettings = resolveQueueSettings({
    +    cfg,
    +    channel: command.channel,
    +    sessionEntry,
    +  });
    +  const queueKey = sessionKey ?? sessionEntry?.sessionId;
    +  const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
    +  const queueOverrides = Boolean(
    +    sessionEntry?.queueDebounceMs ??
    +      sessionEntry?.queueCap ??
    +      sessionEntry?.queueDrop,
    +  );
    +  const groupActivation = isGroup
    +    ? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
    +      defaultGroupActivation())
    +    : undefined;
    +  const agentDefaults = cfg.agents?.defaults ?? {};
    +  const statusText = buildStatusMessage({
    +    config: cfg,
    +    agent: {
    +      ...agentDefaults,
    +      model: {
    +        ...agentDefaults.model,
    +        primary: `${provider}/${model}`,
    +      },
    +      contextTokens,
    +      thinkingDefault: agentDefaults.thinkingDefault,
    +      verboseDefault: agentDefaults.verboseDefault,
    +      elevatedDefault: agentDefaults.elevatedDefault,
    +    },
    +    sessionEntry,
    +    sessionKey,
    +    sessionScope,
    +    groupActivation,
    +    resolvedThink: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
    +    resolvedVerbose: resolvedVerboseLevel,
    +    resolvedReasoning: resolvedReasoningLevel,
    +    resolvedElevated: resolvedElevatedLevel,
    +    modelAuth: resolveModelAuthLabel(
    +      provider,
    +      cfg,
    +      sessionEntry,
    +      statusAgentDir,
    +    ),
    +    usageLine: usageLine ?? undefined,
    +    queue: {
    +      mode: queueSettings.mode,
    +      depth: queueDepth,
    +      debounceMs: queueSettings.debounceMs,
    +      cap: queueSettings.cap,
    +      dropPolicy: queueSettings.dropPolicy,
    +      showDetails: queueOverrides,
    +    },
    +    includeTranscriptUsage: false,
    +  });
    +  return { text: statusText };
    +}
    
  • src/auto-reply/reply/commands.ts+8 1068 modified
    @@ -1,1068 +1,8 @@
    -import {
    -  resolveAgentDir,
    -  resolveDefaultAgentId,
    -  resolveSessionAgentId,
    -} from "../../agents/agent-scope.js";
    -import {
    -  ensureAuthProfileStore,
    -  resolveAuthProfileDisplayLabel,
    -  resolveAuthProfileOrder,
    -} from "../../agents/auth-profiles.js";
    -import {
    -  getCustomProviderApiKey,
    -  resolveEnvApiKey,
    -} from "../../agents/model-auth.js";
    -import { normalizeProviderId } from "../../agents/model-selection.js";
    -import {
    -  abortEmbeddedPiRun,
    -  compactEmbeddedPiSession,
    -  isEmbeddedPiRunActive,
    -  waitForEmbeddedPiRunEnd,
    -} from "../../agents/pi-embedded.js";
    -import type { ChannelId } from "../../channels/plugins/types.js";
    -import type { ClawdbotConfig } from "../../config/config.js";
    -import {
    -  readConfigFileSnapshot,
    -  validateConfigObject,
    -  writeConfigFile,
    -} from "../../config/config.js";
    -import {
    -  getConfigValueAtPath,
    -  parseConfigPath,
    -  setConfigValueAtPath,
    -  unsetConfigValueAtPath,
    -} from "../../config/config-paths.js";
    -import {
    -  getConfigOverrides,
    -  resetConfigOverrides,
    -  setConfigOverride,
    -  unsetConfigOverride,
    -} from "../../config/runtime-overrides.js";
    -import {
    -  resolveSessionFilePath,
    -  type SessionEntry,
    -  type SessionScope,
    -  saveSessionStore,
    -} from "../../config/sessions.js";
    -import { logVerbose } from "../../globals.js";
    -import {
    -  formatUsageSummaryLine,
    -  loadProviderUsageSummary,
    -  resolveUsageProviderId,
    -} from "../../infra/provider-usage.js";
    -import {
    -  scheduleGatewaySigusr1Restart,
    -  triggerClawdbotRestart,
    -} from "../../infra/restart.js";
    -import { enqueueSystemEvent } from "../../infra/system-events.js";
    -import { parseAgentSessionKey } from "../../routing/session-key.js";
    -import { resolveSendPolicy } from "../../sessions/send-policy.js";
    -import { resolveCommandAuthorization } from "../command-auth.js";
    -import {
    -  normalizeCommandBody,
    -  shouldHandleTextCommands,
    -} from "../commands-registry.js";
    -import {
    -  normalizeGroupActivation,
    -  parseActivationCommand,
    -} from "../group-activation.js";
    -import { parseSendPolicyCommand } from "../send-policy.js";
    -import {
    -  buildCommandsMessage,
    -  buildHelpMessage,
    -  buildStatusMessage,
    -  formatContextUsageShort,
    -  formatTokenCount,
    -} from "../status.js";
    -import type { MsgContext } from "../templating.js";
    -import type {
    -  ElevatedLevel,
    -  ReasoningLevel,
    -  ThinkLevel,
    -  VerboseLevel,
    -} from "../thinking.js";
    -import type { ReplyPayload } from "../types.js";
    -import { isAbortTrigger, setAbortMemory } from "./abort.js";
    -import { handleBashChatCommand } from "./bash-command.js";
    -import { parseConfigCommand } from "./config-commands.js";
    -import { parseDebugCommand } from "./debug-commands.js";
    -import type { InlineDirectives } from "./directive-handling.js";
    -import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
    -import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js";
    -import { incrementCompactionCount } from "./session-updates.js";
    -
    -function resolveSessionEntryForKey(
    -  store: Record<string, SessionEntry> | undefined,
    -  sessionKey: string | undefined,
    -) {
    -  if (!store || !sessionKey) return {};
    -  const direct = store[sessionKey];
    -  if (direct) return { entry: direct, key: sessionKey };
    -  const parsed = parseAgentSessionKey(sessionKey);
    -  const legacyKey = parsed?.rest;
    -  if (legacyKey && store[legacyKey]) {
    -    return { entry: store[legacyKey], key: legacyKey };
    -  }
    -  return {};
    -}
    -
    -export type CommandContext = {
    -  surface: string;
    -  channel: string;
    -  channelId?: ChannelId;
    -  ownerList: string[];
    -  isAuthorizedSender: boolean;
    -  senderId?: string;
    -  abortKey?: string;
    -  rawBodyNormalized: string;
    -  commandBodyNormalized: string;
    -  from?: string;
    -  to?: string;
    -};
    -
    -export async function buildStatusReply(params: {
    -  cfg: ClawdbotConfig;
    -  command: CommandContext;
    -  sessionEntry?: SessionEntry;
    -  sessionKey: string;
    -  sessionScope?: SessionScope;
    -  provider: string;
    -  model: string;
    -  contextTokens: number;
    -  resolvedThinkLevel?: ThinkLevel;
    -  resolvedVerboseLevel: VerboseLevel;
    -  resolvedReasoningLevel: ReasoningLevel;
    -  resolvedElevatedLevel?: ElevatedLevel;
    -  resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
    -  isGroup: boolean;
    -  defaultGroupActivation: () => "always" | "mention";
    -}): Promise<ReplyPayload | undefined> {
    -  const {
    -    cfg,
    -    command,
    -    sessionEntry,
    -    sessionKey,
    -    sessionScope,
    -    provider,
    -    model,
    -    contextTokens,
    -    resolvedThinkLevel,
    -    resolvedVerboseLevel,
    -    resolvedReasoningLevel,
    -    resolvedElevatedLevel,
    -    resolveDefaultThinkingLevel,
    -    isGroup,
    -    defaultGroupActivation,
    -  } = params;
    -  if (!command.isAuthorizedSender) {
    -    logVerbose(
    -      `Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -    );
    -    return undefined;
    -  }
    -  const statusAgentId = sessionKey
    -    ? resolveSessionAgentId({ sessionKey, config: cfg })
    -    : resolveDefaultAgentId(cfg);
    -  const statusAgentDir = resolveAgentDir(cfg, statusAgentId);
    -  let usageLine: string | null = null;
    -  try {
    -    const usageProvider = resolveUsageProviderId(provider);
    -    if (usageProvider) {
    -      const usageSummary = await loadProviderUsageSummary({
    -        timeoutMs: 3500,
    -        providers: [usageProvider],
    -        agentDir: statusAgentDir,
    -      });
    -      usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() });
    -      if (
    -        !usageLine &&
    -        (resolvedVerboseLevel === "on" || resolvedElevatedLevel === "on")
    -      ) {
    -        const entry = usageSummary.providers[0];
    -        if (entry?.error) {
    -          usageLine = `📊 Usage: ${entry.displayName} (${entry.error})`;
    -        }
    -      }
    -    }
    -  } catch {
    -    usageLine = null;
    -  }
    -  const queueSettings = resolveQueueSettings({
    -    cfg,
    -    channel: command.channel,
    -    sessionEntry,
    -  });
    -  const queueKey = sessionKey ?? sessionEntry?.sessionId;
    -  const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
    -  const queueOverrides = Boolean(
    -    sessionEntry?.queueDebounceMs ??
    -      sessionEntry?.queueCap ??
    -      sessionEntry?.queueDrop,
    -  );
    -  const groupActivation = isGroup
    -    ? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
    -      defaultGroupActivation())
    -    : undefined;
    -  const agentDefaults = cfg.agents?.defaults ?? {};
    -  const statusText = buildStatusMessage({
    -    config: cfg,
    -    agent: {
    -      ...agentDefaults,
    -      model: {
    -        ...agentDefaults.model,
    -        primary: `${provider}/${model}`,
    -      },
    -      contextTokens,
    -      thinkingDefault: agentDefaults.thinkingDefault,
    -      verboseDefault: agentDefaults.verboseDefault,
    -      elevatedDefault: agentDefaults.elevatedDefault,
    -    },
    -    sessionEntry,
    -    sessionKey,
    -    sessionScope,
    -    groupActivation,
    -    resolvedThink: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
    -    resolvedVerbose: resolvedVerboseLevel,
    -    resolvedReasoning: resolvedReasoningLevel,
    -    resolvedElevated: resolvedElevatedLevel,
    -    modelAuth: resolveModelAuthLabel(
    -      provider,
    -      cfg,
    -      sessionEntry,
    -      statusAgentDir,
    -    ),
    -    usageLine: usageLine ?? undefined,
    -    queue: {
    -      mode: queueSettings.mode,
    -      depth: queueDepth,
    -      debounceMs: queueSettings.debounceMs,
    -      cap: queueSettings.cap,
    -      dropPolicy: queueSettings.dropPolicy,
    -      showDetails: queueOverrides,
    -    },
    -    includeTranscriptUsage: false,
    -  });
    -  return { text: statusText };
    -}
    -
    -function formatApiKeySnippet(apiKey: string): string {
    -  const compact = apiKey.replace(/\s+/g, "");
    -  if (!compact) return "unknown";
    -  const edge = compact.length >= 12 ? 6 : 4;
    -  const head = compact.slice(0, edge);
    -  const tail = compact.slice(-edge);
    -  return `${head}…${tail}`;
    -}
    -
    -function resolveModelAuthLabel(
    -  provider?: string,
    -  cfg?: ClawdbotConfig,
    -  sessionEntry?: SessionEntry,
    -  agentDir?: string,
    -): string | undefined {
    -  const resolved = provider?.trim();
    -  if (!resolved) return undefined;
    -
    -  const providerKey = normalizeProviderId(resolved);
    -  const store = ensureAuthProfileStore(agentDir, {
    -    allowKeychainPrompt: false,
    -  });
    -  const profileOverride = sessionEntry?.authProfileOverride?.trim();
    -  const order = resolveAuthProfileOrder({
    -    cfg,
    -    store,
    -    provider: providerKey,
    -    preferredProfile: profileOverride,
    -  });
    -  const candidates = [profileOverride, ...order].filter(Boolean) as string[];
    -
    -  for (const profileId of candidates) {
    -    const profile = store.profiles[profileId];
    -    if (!profile || normalizeProviderId(profile.provider) !== providerKey) {
    -      continue;
    -    }
    -    const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
    -    if (profile.type === "oauth") {
    -      return `oauth${label ? ` (${label})` : ""}`;
    -    }
    -    if (profile.type === "token") {
    -      const snippet = formatApiKeySnippet(profile.token);
    -      return `token ${snippet}${label ? ` (${label})` : ""}`;
    -    }
    -    const snippet = formatApiKeySnippet(profile.key);
    -    return `api-key ${snippet}${label ? ` (${label})` : ""}`;
    -  }
    -
    -  const envKey = resolveEnvApiKey(providerKey);
    -  if (envKey?.apiKey) {
    -    if (envKey.source.includes("OAUTH_TOKEN")) {
    -      return `oauth (${envKey.source})`;
    -    }
    -    return `api-key ${formatApiKeySnippet(envKey.apiKey)} (${envKey.source})`;
    -  }
    -
    -  const customKey = getCustomProviderApiKey(cfg, providerKey);
    -  if (customKey) {
    -    return `api-key ${formatApiKeySnippet(customKey)} (models.json)`;
    -  }
    -
    -  return "unknown";
    -}
    -
    -function extractCompactInstructions(params: {
    -  rawBody?: string;
    -  ctx: MsgContext;
    -  cfg: ClawdbotConfig;
    -  agentId?: string;
    -  isGroup: boolean;
    -}): string | undefined {
    -  const raw = stripStructuralPrefixes(params.rawBody ?? "");
    -  const stripped = params.isGroup
    -    ? stripMentions(raw, params.ctx, params.cfg, params.agentId)
    -    : raw;
    -  const trimmed = stripped.trim();
    -  if (!trimmed) return undefined;
    -  const lowered = trimmed.toLowerCase();
    -  const prefix = lowered.startsWith("/compact") ? "/compact" : null;
    -  if (!prefix) return undefined;
    -  let rest = trimmed.slice(prefix.length).trimStart();
    -  if (rest.startsWith(":")) rest = rest.slice(1).trimStart();
    -  return rest.length ? rest : undefined;
    -}
    -
    -export function buildCommandContext(params: {
    -  ctx: MsgContext;
    -  cfg: ClawdbotConfig;
    -  agentId?: string;
    -  sessionKey?: string;
    -  isGroup: boolean;
    -  triggerBodyNormalized: string;
    -  commandAuthorized: boolean;
    -}): CommandContext {
    -  const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } =
    -    params;
    -  const auth = resolveCommandAuthorization({
    -    ctx,
    -    cfg,
    -    commandAuthorized: params.commandAuthorized,
    -  });
    -  const surface = (ctx.Surface ?? ctx.Provider ?? "").trim().toLowerCase();
    -  const channel = (ctx.Provider ?? surface).trim().toLowerCase();
    -  const abortKey =
    -    sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
    -  const rawBodyNormalized = triggerBodyNormalized;
    -  const commandBodyNormalized = normalizeCommandBody(
    -    isGroup
    -      ? stripMentions(rawBodyNormalized, ctx, cfg, agentId)
    -      : rawBodyNormalized,
    -  );
    -
    -  return {
    -    surface,
    -    channel,
    -    channelId: auth.providerId,
    -    ownerList: auth.ownerList,
    -    isAuthorizedSender: auth.isAuthorizedSender,
    -    senderId: auth.senderId,
    -    abortKey,
    -    rawBodyNormalized,
    -    commandBodyNormalized,
    -    from: auth.from,
    -    to: auth.to,
    -  };
    -}
    -
    -function resolveAbortTarget(params: {
    -  ctx: MsgContext;
    -  sessionKey?: string;
    -  sessionEntry?: SessionEntry;
    -  sessionStore?: Record<string, SessionEntry>;
    -}) {
    -  const targetSessionKey =
    -    params.ctx.CommandTargetSessionKey?.trim() || params.sessionKey;
    -  const { entry, key } = resolveSessionEntryForKey(
    -    params.sessionStore,
    -    targetSessionKey,
    -  );
    -  if (entry && key) return { entry, key, sessionId: entry.sessionId };
    -  if (params.sessionEntry && params.sessionKey) {
    -    return {
    -      entry: params.sessionEntry,
    -      key: params.sessionKey,
    -      sessionId: params.sessionEntry.sessionId,
    -    };
    -  }
    -  return { entry: undefined, key: targetSessionKey, sessionId: undefined };
    -}
    -
    -export async function handleCommands(params: {
    -  ctx: MsgContext;
    -  cfg: ClawdbotConfig;
    -  command: CommandContext;
    -  agentId?: string;
    -  directives: InlineDirectives;
    -  elevated: {
    -    enabled: boolean;
    -    allowed: boolean;
    -    failures: Array<{ gate: string; key: string }>;
    -  };
    -  sessionEntry?: SessionEntry;
    -  sessionStore?: Record<string, SessionEntry>;
    -  sessionKey: string;
    -  storePath?: string;
    -  sessionScope?: SessionScope;
    -  workspaceDir: string;
    -  defaultGroupActivation: () => "always" | "mention";
    -  resolvedThinkLevel?: ThinkLevel;
    -  resolvedVerboseLevel: VerboseLevel;
    -  resolvedReasoningLevel: ReasoningLevel;
    -  resolvedElevatedLevel?: ElevatedLevel;
    -  resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
    -  provider: string;
    -  model: string;
    -  contextTokens: number;
    -  isGroup: boolean;
    -}): Promise<{
    -  reply?: ReplyPayload;
    -  shouldContinue: boolean;
    -}> {
    -  const {
    -    ctx,
    -    cfg,
    -    command,
    -    directives,
    -    elevated,
    -    sessionEntry,
    -    sessionStore,
    -    sessionKey,
    -    storePath,
    -    sessionScope,
    -    workspaceDir,
    -    defaultGroupActivation,
    -    resolvedThinkLevel,
    -    resolvedVerboseLevel,
    -    resolvedReasoningLevel,
    -    resolvedElevatedLevel,
    -    resolveDefaultThinkingLevel,
    -    provider,
    -    model,
    -    contextTokens,
    -    isGroup,
    -  } = params;
    -
    -  const resetRequested =
    -    command.commandBodyNormalized === "/reset" ||
    -    command.commandBodyNormalized === "/new";
    -  if (resetRequested && !command.isAuthorizedSender) {
    -    logVerbose(
    -      `Ignoring /reset from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -    );
    -    return { shouldContinue: false };
    -  }
    -
    -  const activationCommand = parseActivationCommand(
    -    command.commandBodyNormalized,
    -  );
    -  const sendPolicyCommand = parseSendPolicyCommand(
    -    command.commandBodyNormalized,
    -  );
    -  const allowTextCommands = shouldHandleTextCommands({
    -    cfg,
    -    surface: command.surface,
    -    commandSource: ctx.CommandSource,
    -  });
    -
    -  const bashSlashRequested =
    -    allowTextCommands &&
    -    (command.commandBodyNormalized === "/bash" ||
    -      command.commandBodyNormalized.startsWith("/bash "));
    -  const bashBangRequested =
    -    allowTextCommands && command.commandBodyNormalized.startsWith("!");
    -  if (bashSlashRequested || (bashBangRequested && command.isAuthorizedSender)) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /bash from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    const reply = await handleBashChatCommand({
    -      ctx,
    -      cfg,
    -      agentId: params.agentId,
    -      sessionKey,
    -      isGroup,
    -      elevated,
    -    });
    -    return { shouldContinue: false, reply };
    -  }
    -
    -  if (allowTextCommands && activationCommand.hasCommand) {
    -    if (!isGroup) {
    -      return {
    -        shouldContinue: false,
    -        reply: { text: "⚙️ Group activation only applies to group chats." },
    -      };
    -    }
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /activation from unauthorized sender in group: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    if (!activationCommand.mode) {
    -      return {
    -        shouldContinue: false,
    -        reply: { text: "⚙️ Usage: /activation mention|always" },
    -      };
    -    }
    -    if (sessionEntry && sessionStore && sessionKey) {
    -      sessionEntry.groupActivation = activationCommand.mode;
    -      sessionEntry.groupActivationNeedsSystemIntro = true;
    -      sessionEntry.updatedAt = Date.now();
    -      sessionStore[sessionKey] = sessionEntry;
    -      if (storePath) {
    -        await saveSessionStore(storePath, sessionStore);
    -      }
    -    }
    -    return {
    -      shouldContinue: false,
    -      reply: { text: `⚙️ Group activation set to ${activationCommand.mode}.` },
    -    };
    -  }
    -
    -  if (allowTextCommands && sendPolicyCommand.hasCommand) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /send from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    if (!sendPolicyCommand.mode) {
    -      return {
    -        shouldContinue: false,
    -        reply: { text: "⚙️ Usage: /send on|off|inherit" },
    -      };
    -    }
    -    if (sessionEntry && sessionStore && sessionKey) {
    -      if (sendPolicyCommand.mode === "inherit") {
    -        delete sessionEntry.sendPolicy;
    -      } else {
    -        sessionEntry.sendPolicy = sendPolicyCommand.mode;
    -      }
    -      sessionEntry.updatedAt = Date.now();
    -      sessionStore[sessionKey] = sessionEntry;
    -      if (storePath) {
    -        await saveSessionStore(storePath, sessionStore);
    -      }
    -    }
    -    const label =
    -      sendPolicyCommand.mode === "inherit"
    -        ? "inherit"
    -        : sendPolicyCommand.mode === "allow"
    -          ? "on"
    -          : "off";
    -    return {
    -      shouldContinue: false,
    -      reply: { text: `⚙️ Send policy set to ${label}.` },
    -    };
    -  }
    -
    -  if (allowTextCommands && command.commandBodyNormalized === "/restart") {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /restart from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    if (cfg.commands?.restart !== true) {
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: "⚠️ /restart is disabled. Set commands.restart=true to enable.",
    -        },
    -      };
    -    }
    -    const hasSigusr1Listener = process.listenerCount("SIGUSR1") > 0;
    -    if (hasSigusr1Listener) {
    -      scheduleGatewaySigusr1Restart({ reason: "/restart" });
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: "⚙️ Restarting clawdbot in-process (SIGUSR1); back in a few seconds.",
    -        },
    -      };
    -    }
    -    const restartMethod = triggerClawdbotRestart();
    -    if (!restartMethod.ok) {
    -      const detail = restartMethod.detail
    -        ? ` Details: ${restartMethod.detail}`
    -        : "";
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: `⚠️ Restart failed (${restartMethod.method}).${detail}`,
    -        },
    -      };
    -    }
    -    return {
    -      shouldContinue: false,
    -      reply: {
    -        text: `⚙️ Restarting clawdbot via ${restartMethod.method}; give me a few seconds to come back online.`,
    -      },
    -    };
    -  }
    -
    -  const helpRequested = command.commandBodyNormalized === "/help";
    -  if (allowTextCommands && helpRequested) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /help from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    return { shouldContinue: false, reply: { text: buildHelpMessage(cfg) } };
    -  }
    -
    -  const commandsRequested = command.commandBodyNormalized === "/commands";
    -  if (allowTextCommands && commandsRequested) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /commands from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    return {
    -      shouldContinue: false,
    -      reply: { text: buildCommandsMessage(cfg) },
    -    };
    -  }
    -
    -  const statusRequested =
    -    directives.hasStatusDirective ||
    -    command.commandBodyNormalized === "/status";
    -  if (allowTextCommands && statusRequested) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    const reply = await buildStatusReply({
    -      cfg,
    -      command,
    -      sessionEntry,
    -      sessionKey,
    -      sessionScope,
    -      provider,
    -      model,
    -      contextTokens,
    -      resolvedThinkLevel,
    -      resolvedVerboseLevel,
    -      resolvedReasoningLevel,
    -      resolvedElevatedLevel,
    -      resolveDefaultThinkingLevel,
    -      isGroup,
    -      defaultGroupActivation,
    -    });
    -    return { shouldContinue: false, reply };
    -  }
    -
    -  const whoamiRequested = command.commandBodyNormalized === "/whoami";
    -  if (allowTextCommands && whoamiRequested) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /whoami from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    const senderId = ctx.SenderId ?? "";
    -    const senderUsername = ctx.SenderUsername ?? "";
    -    const lines = ["🧭 Identity", `Channel: ${command.channel}`];
    -    if (senderId) lines.push(`User id: ${senderId}`);
    -    if (senderUsername) {
    -      const handle = senderUsername.startsWith("@")
    -        ? senderUsername
    -        : `@${senderUsername}`;
    -      lines.push(`Username: ${handle}`);
    -    }
    -    if (ctx.ChatType === "group" && ctx.From) {
    -      lines.push(`Chat: ${ctx.From}`);
    -    }
    -    if (ctx.MessageThreadId != null) {
    -      lines.push(`Thread: ${ctx.MessageThreadId}`);
    -    }
    -    if (senderId) {
    -      lines.push(`AllowFrom: ${senderId}`);
    -    }
    -    return { shouldContinue: false, reply: { text: lines.join("\n") } };
    -  }
    -
    -  const configCommand = allowTextCommands
    -    ? parseConfigCommand(command.commandBodyNormalized)
    -    : null;
    -  if (configCommand) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /config from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    if (cfg.commands?.config !== true) {
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: "⚠️ /config is disabled. Set commands.config=true to enable.",
    -        },
    -      };
    -    }
    -    if (configCommand.action === "error") {
    -      return {
    -        shouldContinue: false,
    -        reply: { text: `⚠️ ${configCommand.message}` },
    -      };
    -    }
    -    const snapshot = await readConfigFileSnapshot();
    -    if (
    -      !snapshot.valid ||
    -      !snapshot.parsed ||
    -      typeof snapshot.parsed !== "object"
    -    ) {
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: "⚠️ Config file is invalid; fix it before using /config.",
    -        },
    -      };
    -    }
    -    const parsedBase = structuredClone(
    -      snapshot.parsed as Record<string, unknown>,
    -    );
    -
    -    if (configCommand.action === "show") {
    -      const pathRaw = configCommand.path?.trim();
    -      if (pathRaw) {
    -        const parsedPath = parseConfigPath(pathRaw);
    -        if (!parsedPath.ok || !parsedPath.path) {
    -          return {
    -            shouldContinue: false,
    -            reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
    -          };
    -        }
    -        const value = getConfigValueAtPath(parsedBase, parsedPath.path);
    -        const rendered = JSON.stringify(value ?? null, null, 2);
    -        return {
    -          shouldContinue: false,
    -          reply: {
    -            text: `⚙️ Config ${pathRaw}:\n\`\`\`json\n${rendered}\n\`\`\``,
    -          },
    -        };
    -      }
    -      const json = JSON.stringify(parsedBase, null, 2);
    -      return {
    -        shouldContinue: false,
    -        reply: { text: `⚙️ Config (raw):\n\`\`\`json\n${json}\n\`\`\`` },
    -      };
    -    }
    -
    -    if (configCommand.action === "unset") {
    -      const parsedPath = parseConfigPath(configCommand.path);
    -      if (!parsedPath.ok || !parsedPath.path) {
    -        return {
    -          shouldContinue: false,
    -          reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
    -        };
    -      }
    -      const removed = unsetConfigValueAtPath(parsedBase, parsedPath.path);
    -      if (!removed) {
    -        return {
    -          shouldContinue: false,
    -          reply: { text: `⚙️ No config value found for ${configCommand.path}.` },
    -        };
    -      }
    -      const validated = validateConfigObject(parsedBase);
    -      if (!validated.ok) {
    -        const issue = validated.issues[0];
    -        return {
    -          shouldContinue: false,
    -          reply: {
    -            text: `⚠️ Config invalid after unset (${issue.path}: ${issue.message}).`,
    -          },
    -        };
    -      }
    -      await writeConfigFile(validated.config);
    -      return {
    -        shouldContinue: false,
    -        reply: { text: `⚙️ Config updated: ${configCommand.path} removed.` },
    -      };
    -    }
    -
    -    if (configCommand.action === "set") {
    -      const parsedPath = parseConfigPath(configCommand.path);
    -      if (!parsedPath.ok || !parsedPath.path) {
    -        return {
    -          shouldContinue: false,
    -          reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
    -        };
    -      }
    -      setConfigValueAtPath(parsedBase, parsedPath.path, configCommand.value);
    -      const validated = validateConfigObject(parsedBase);
    -      if (!validated.ok) {
    -        const issue = validated.issues[0];
    -        return {
    -          shouldContinue: false,
    -          reply: {
    -            text: `⚠️ Config invalid after set (${issue.path}: ${issue.message}).`,
    -          },
    -        };
    -      }
    -      await writeConfigFile(validated.config);
    -      const valueLabel =
    -        typeof configCommand.value === "string"
    -          ? `"${configCommand.value}"`
    -          : JSON.stringify(configCommand.value);
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: `⚙️ Config updated: ${configCommand.path}=${valueLabel ?? "null"}`,
    -        },
    -      };
    -    }
    -  }
    -
    -  const debugCommand = allowTextCommands
    -    ? parseDebugCommand(command.commandBodyNormalized)
    -    : null;
    -  if (debugCommand) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /debug from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    if (cfg.commands?.debug !== true) {
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: "⚠️ /debug is disabled. Set commands.debug=true to enable.",
    -        },
    -      };
    -    }
    -    if (debugCommand.action === "error") {
    -      return {
    -        shouldContinue: false,
    -        reply: { text: `⚠️ ${debugCommand.message}` },
    -      };
    -    }
    -    if (debugCommand.action === "show") {
    -      const overrides = getConfigOverrides();
    -      const hasOverrides = Object.keys(overrides).length > 0;
    -      if (!hasOverrides) {
    -        return {
    -          shouldContinue: false,
    -          reply: { text: "⚙️ Debug overrides: (none)" },
    -        };
    -      }
    -      const json = JSON.stringify(overrides, null, 2);
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: `⚙️ Debug overrides (memory-only):\n\`\`\`json\n${json}\n\`\`\``,
    -        },
    -      };
    -    }
    -    if (debugCommand.action === "reset") {
    -      resetConfigOverrides();
    -      return {
    -        shouldContinue: false,
    -        reply: { text: "⚙️ Debug overrides cleared; using config on disk." },
    -      };
    -    }
    -    if (debugCommand.action === "unset") {
    -      const result = unsetConfigOverride(debugCommand.path);
    -      if (!result.ok) {
    -        return {
    -          shouldContinue: false,
    -          reply: { text: `⚠️ ${result.error ?? "Invalid path."}` },
    -        };
    -      }
    -      if (!result.removed) {
    -        return {
    -          shouldContinue: false,
    -          reply: {
    -            text: `⚙️ No debug override found for ${debugCommand.path}.`,
    -          },
    -        };
    -      }
    -      return {
    -        shouldContinue: false,
    -        reply: { text: `⚙️ Debug override removed for ${debugCommand.path}.` },
    -      };
    -    }
    -    if (debugCommand.action === "set") {
    -      const result = setConfigOverride(debugCommand.path, debugCommand.value);
    -      if (!result.ok) {
    -        return {
    -          shouldContinue: false,
    -          reply: { text: `⚠️ ${result.error ?? "Invalid override."}` },
    -        };
    -      }
    -      const valueLabel =
    -        typeof debugCommand.value === "string"
    -          ? `"${debugCommand.value}"`
    -          : JSON.stringify(debugCommand.value);
    -      return {
    -        shouldContinue: false,
    -        reply: {
    -          text: `⚙️ Debug override set: ${debugCommand.path}=${valueLabel ?? "null"}`,
    -        },
    -      };
    -    }
    -  }
    -
    -  const stopRequested = command.commandBodyNormalized === "/stop";
    -  if (allowTextCommands && stopRequested) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /stop from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    const abortTarget = resolveAbortTarget({
    -      ctx,
    -      sessionKey,
    -      sessionEntry,
    -      sessionStore,
    -    });
    -    if (abortTarget.sessionId) {
    -      abortEmbeddedPiRun(abortTarget.sessionId);
    -    }
    -    if (abortTarget.entry && sessionStore && abortTarget.key) {
    -      abortTarget.entry.abortedLastRun = true;
    -      abortTarget.entry.updatedAt = Date.now();
    -      sessionStore[abortTarget.key] = abortTarget.entry;
    -      if (storePath) {
    -        await saveSessionStore(storePath, sessionStore);
    -      }
    -    } else if (command.abortKey) {
    -      setAbortMemory(command.abortKey, true);
    -    }
    -    return { shouldContinue: false, reply: { text: "⚙️ Agent was aborted." } };
    -  }
    -
    -  const compactRequested =
    -    command.commandBodyNormalized === "/compact" ||
    -    command.commandBodyNormalized.startsWith("/compact ");
    -  if (compactRequested) {
    -    if (!command.isAuthorizedSender) {
    -      logVerbose(
    -        `Ignoring /compact from unauthorized sender: ${command.senderId || "<unknown>"}`,
    -      );
    -      return { shouldContinue: false };
    -    }
    -    if (!sessionEntry?.sessionId) {
    -      return {
    -        shouldContinue: false,
    -        reply: { text: "⚙️ Compaction unavailable (missing session id)." },
    -      };
    -    }
    -    const sessionId = sessionEntry.sessionId;
    -    if (isEmbeddedPiRunActive(sessionId)) {
    -      abortEmbeddedPiRun(sessionId);
    -      await waitForEmbeddedPiRunEnd(sessionId, 15_000);
    -    }
    -    const customInstructions = extractCompactInstructions({
    -      rawBody: ctx.CommandBody ?? ctx.RawBody ?? ctx.Body,
    -      ctx,
    -      cfg,
    -      agentId: params.agentId,
    -      isGroup,
    -    });
    -    const result = await compactEmbeddedPiSession({
    -      sessionId,
    -      sessionKey,
    -      messageChannel: command.channel,
    -      sessionFile: resolveSessionFilePath(sessionId, sessionEntry),
    -      workspaceDir,
    -      config: cfg,
    -      skillsSnapshot: sessionEntry.skillsSnapshot,
    -      provider,
    -      model,
    -      thinkLevel: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
    -      bashElevated: {
    -        enabled: false,
    -        allowed: false,
    -        defaultLevel: "off",
    -      },
    -      customInstructions,
    -      ownerNumbers:
    -        command.ownerList.length > 0 ? command.ownerList : undefined,
    -    });
    -
    -    const totalTokens =
    -      sessionEntry.totalTokens ??
    -      (sessionEntry.inputTokens ?? 0) + (sessionEntry.outputTokens ?? 0);
    -    const contextSummary = formatContextUsageShort(
    -      totalTokens > 0 ? totalTokens : null,
    -      contextTokens ?? sessionEntry.contextTokens ?? null,
    -    );
    -    const compactLabel = result.ok
    -      ? result.compacted
    -        ? result.result?.tokensBefore
    -          ? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
    -          : "Compacted"
    -        : "Compaction skipped"
    -      : "Compaction failed";
    -    if (result.ok && result.compacted) {
    -      await incrementCompactionCount({
    -        sessionEntry,
    -        sessionStore,
    -        sessionKey,
    -        storePath,
    -      });
    -    }
    -    const reason = result.reason?.trim();
    -    const line = reason
    -      ? `${compactLabel}: ${reason} • ${contextSummary}`
    -      : `${compactLabel} • ${contextSummary}`;
    -    enqueueSystemEvent(line, { sessionKey });
    -    return { shouldContinue: false, reply: { text: `⚙️ ${line}` } };
    -  }
    -
    -  const abortRequested = isAbortTrigger(command.rawBodyNormalized);
    -  if (allowTextCommands && abortRequested) {
    -    const abortTarget = resolveAbortTarget({
    -      ctx,
    -      sessionKey,
    -      sessionEntry,
    -      sessionStore,
    -    });
    -    if (abortTarget.sessionId) {
    -      abortEmbeddedPiRun(abortTarget.sessionId);
    -    }
    -    if (abortTarget.entry && sessionStore && abortTarget.key) {
    -      abortTarget.entry.abortedLastRun = true;
    -      abortTarget.entry.updatedAt = Date.now();
    -      sessionStore[abortTarget.key] = abortTarget.entry;
    -      if (storePath) {
    -        await saveSessionStore(storePath, sessionStore);
    -      }
    -    } else if (command.abortKey) {
    -      setAbortMemory(command.abortKey, true);
    -    }
    -    return { shouldContinue: false, reply: { text: "⚙️ Agent was aborted." } };
    -  }
    -
    -  const sendPolicy = resolveSendPolicy({
    -    cfg,
    -    entry: sessionEntry,
    -    sessionKey,
    -    channel: sessionEntry?.channel ?? command.channel,
    -    chatType: sessionEntry?.chatType,
    -  });
    -  if (sendPolicy === "deny") {
    -    logVerbose(`Send blocked by policy for session ${sessionKey ?? "unknown"}`);
    -    return { shouldContinue: false };
    -  }
    -
    -  return { shouldContinue: true };
    -}
    +export { buildCommandContext } from "./commands-context.js";
    +export { handleCommands } from "./commands-core.js";
    +export { buildStatusReply } from "./commands-status.js";
    +export type {
    +  CommandContext,
    +  CommandHandlerResult,
    +  HandleCommandsParams,
    +} from "./commands-types.js";
    
  • src/auto-reply/reply/commands-types.ts+65 0 added
    @@ -0,0 +1,65 @@
    +import type { ChannelId } from "../../channels/plugins/types.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import type { SessionEntry, SessionScope } from "../../config/sessions.js";
    +import type { MsgContext } from "../templating.js";
    +import type {
    +  ElevatedLevel,
    +  ReasoningLevel,
    +  ThinkLevel,
    +  VerboseLevel,
    +} from "../thinking.js";
    +import type { ReplyPayload } from "../types.js";
    +import type { InlineDirectives } from "./directive-handling.js";
    +
    +export type CommandContext = {
    +  surface: string;
    +  channel: string;
    +  channelId?: ChannelId;
    +  ownerList: string[];
    +  isAuthorizedSender: boolean;
    +  senderId?: string;
    +  abortKey?: string;
    +  rawBodyNormalized: string;
    +  commandBodyNormalized: string;
    +  from?: string;
    +  to?: string;
    +};
    +
    +export type HandleCommandsParams = {
    +  ctx: MsgContext;
    +  cfg: ClawdbotConfig;
    +  command: CommandContext;
    +  agentId?: string;
    +  directives: InlineDirectives;
    +  elevated: {
    +    enabled: boolean;
    +    allowed: boolean;
    +    failures: Array<{ gate: string; key: string }>;
    +  };
    +  sessionEntry?: SessionEntry;
    +  sessionStore?: Record<string, SessionEntry>;
    +  sessionKey: string;
    +  storePath?: string;
    +  sessionScope?: SessionScope;
    +  workspaceDir: string;
    +  defaultGroupActivation: () => "always" | "mention";
    +  resolvedThinkLevel?: ThinkLevel;
    +  resolvedVerboseLevel: VerboseLevel;
    +  resolvedReasoningLevel: ReasoningLevel;
    +  resolvedElevatedLevel?: ElevatedLevel;
    +  resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
    +  provider: string;
    +  model: string;
    +  contextTokens: number;
    +  isGroup: boolean;
    +};
    +
    +export type CommandHandlerResult = {
    +  reply?: ReplyPayload;
    +  shouldContinue: boolean;
    +};
    +
    +export type CommandHandler = (
    +  params: HandleCommandsParams,
    +  allowTextCommands: boolean,
    +) => Promise<CommandHandlerResult | null>;
    
  • src/auto-reply/reply/get-reply-directives-apply.ts+309 0 added
    @@ -0,0 +1,309 @@
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import type { SessionEntry } from "../../config/sessions.js";
    +import type { MsgContext } from "../templating.js";
    +import type {
    +  ElevatedLevel,
    +  ReasoningLevel,
    +  ThinkLevel,
    +  VerboseLevel,
    +} from "../thinking.js";
    +import type { ReplyPayload } from "../types.js";
    +import { buildStatusReply } from "./commands.js";
    +import {
    +  applyInlineDirectivesFastLane,
    +  handleDirectiveOnly,
    +  type InlineDirectives,
    +  isDirectiveOnly,
    +  persistInlineDirectives,
    +} from "./directive-handling.js";
    +import type { createModelSelectionState } from "./model-selection.js";
    +import type { TypingController } from "./typing.js";
    +
    +type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
    +
    +export type ApplyDirectiveResult =
    +  | { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
    +  | {
    +      kind: "continue";
    +      directives: InlineDirectives;
    +      provider: string;
    +      model: string;
    +      contextTokens: number;
    +      directiveAck?: ReplyPayload;
    +      perMessageQueueMode?: InlineDirectives["queueMode"];
    +      perMessageQueueOptions?: {
    +        debounceMs?: number;
    +        cap?: number;
    +        dropPolicy?: InlineDirectives["dropPolicy"];
    +      };
    +    };
    +
    +export async function applyInlineDirectiveOverrides(params: {
    +  ctx: MsgContext;
    +  cfg: ClawdbotConfig;
    +  agentId: string;
    +  agentDir: string;
    +  agentCfg: AgentDefaults;
    +  sessionEntry?: SessionEntry;
    +  sessionStore?: Record<string, SessionEntry>;
    +  sessionKey: string;
    +  storePath?: string;
    +  sessionScope: Parameters<typeof buildStatusReply>[0]["sessionScope"];
    +  isGroup: boolean;
    +  allowTextCommands: boolean;
    +  command: Parameters<typeof buildStatusReply>[0]["command"];
    +  directives: InlineDirectives;
    +  messageProviderKey: string;
    +  elevatedEnabled: boolean;
    +  elevatedAllowed: boolean;
    +  elevatedFailures: Array<{ gate: string; key: string }>;
    +  defaultProvider: string;
    +  defaultModel: string;
    +  aliasIndex: Parameters<typeof applyInlineDirectivesFastLane>[0]["aliasIndex"];
    +  provider: string;
    +  model: string;
    +  modelState: Awaited<ReturnType<typeof createModelSelectionState>>;
    +  initialModelLabel: string;
    +  formatModelSwitchEvent: (label: string, alias?: string) => string;
    +  resolvedElevatedLevel: ElevatedLevel;
    +  defaultActivation: () => ReturnType<
    +    Parameters<typeof buildStatusReply>[0]["defaultGroupActivation"]
    +  >;
    +  contextTokens: number;
    +  effectiveModelDirective?: string;
    +  typing: TypingController;
    +}): Promise<ApplyDirectiveResult> {
    +  const {
    +    ctx,
    +    cfg,
    +    agentId,
    +    agentDir,
    +    agentCfg,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    sessionScope,
    +    isGroup,
    +    allowTextCommands,
    +    command,
    +    messageProviderKey,
    +    elevatedEnabled,
    +    elevatedAllowed,
    +    elevatedFailures,
    +    defaultProvider,
    +    defaultModel,
    +    aliasIndex,
    +    modelState,
    +    initialModelLabel,
    +    formatModelSwitchEvent,
    +    resolvedElevatedLevel,
    +    defaultActivation,
    +    typing,
    +    effectiveModelDirective,
    +  } = params;
    +  let { directives } = params;
    +  let { provider, model } = params;
    +  let { contextTokens } = params;
    +
    +  let directiveAck: ReplyPayload | undefined;
    +
    +  if (!command.isAuthorizedSender) {
    +    directives = {
    +      ...directives,
    +      hasThinkDirective: false,
    +      hasVerboseDirective: false,
    +      hasReasoningDirective: false,
    +      hasElevatedDirective: false,
    +      hasStatusDirective: false,
    +      hasModelDirective: false,
    +      hasQueueDirective: false,
    +      queueReset: false,
    +    };
    +  }
    +
    +  if (
    +    isDirectiveOnly({
    +      directives,
    +      cleanedBody: directives.cleaned,
    +      ctx,
    +      cfg,
    +      agentId,
    +      isGroup,
    +    })
    +  ) {
    +    if (!command.isAuthorizedSender) {
    +      typing.cleanup();
    +      return { kind: "reply", reply: undefined };
    +    }
    +    const resolvedDefaultThinkLevel =
    +      (sessionEntry?.thinkingLevel as ThinkLevel | undefined) ??
    +      (agentCfg?.thinkingDefault as ThinkLevel | undefined) ??
    +      (await modelState.resolveDefaultThinkingLevel());
    +    const currentThinkLevel = resolvedDefaultThinkLevel;
    +    const currentVerboseLevel =
    +      (sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
    +      (agentCfg?.verboseDefault as VerboseLevel | undefined);
    +    const currentReasoningLevel =
    +      (sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ?? "off";
    +    const currentElevatedLevel =
    +      (sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
    +      (agentCfg?.elevatedDefault as ElevatedLevel | undefined);
    +    const directiveReply = await handleDirectiveOnly({
    +      cfg,
    +      directives,
    +      sessionEntry,
    +      sessionStore,
    +      sessionKey,
    +      storePath,
    +      elevatedEnabled,
    +      elevatedAllowed,
    +      elevatedFailures,
    +      messageProviderKey,
    +      defaultProvider,
    +      defaultModel,
    +      aliasIndex,
    +      allowedModelKeys: modelState.allowedModelKeys,
    +      allowedModelCatalog: modelState.allowedModelCatalog,
    +      resetModelOverride: modelState.resetModelOverride,
    +      provider,
    +      model,
    +      initialModelLabel,
    +      formatModelSwitchEvent,
    +      currentThinkLevel,
    +      currentVerboseLevel,
    +      currentReasoningLevel,
    +      currentElevatedLevel,
    +    });
    +    let statusReply: ReplyPayload | undefined;
    +    if (
    +      directives.hasStatusDirective &&
    +      allowTextCommands &&
    +      command.isAuthorizedSender
    +    ) {
    +      statusReply = await buildStatusReply({
    +        cfg,
    +        command,
    +        sessionEntry,
    +        sessionKey,
    +        sessionScope,
    +        provider,
    +        model,
    +        contextTokens,
    +        resolvedThinkLevel: resolvedDefaultThinkLevel,
    +        resolvedVerboseLevel: (currentVerboseLevel ?? "off") as VerboseLevel,
    +        resolvedReasoningLevel: (currentReasoningLevel ??
    +          "off") as ReasoningLevel,
    +        resolvedElevatedLevel,
    +        resolveDefaultThinkingLevel: async () => resolvedDefaultThinkLevel,
    +        isGroup,
    +        defaultGroupActivation: defaultActivation,
    +      });
    +    }
    +    typing.cleanup();
    +    if (statusReply?.text && directiveReply?.text) {
    +      return {
    +        kind: "reply",
    +        reply: { text: `${directiveReply.text}\n${statusReply.text}` },
    +      };
    +    }
    +    return { kind: "reply", reply: statusReply ?? directiveReply };
    +  }
    +
    +  const hasAnyDirective =
    +    directives.hasThinkDirective ||
    +    directives.hasVerboseDirective ||
    +    directives.hasReasoningDirective ||
    +    directives.hasElevatedDirective ||
    +    directives.hasModelDirective ||
    +    directives.hasQueueDirective ||
    +    directives.hasStatusDirective;
    +
    +  if (hasAnyDirective && command.isAuthorizedSender) {
    +    const fastLane = await applyInlineDirectivesFastLane({
    +      directives,
    +      commandAuthorized: command.isAuthorizedSender,
    +      ctx,
    +      cfg,
    +      agentId,
    +      isGroup,
    +      sessionEntry,
    +      sessionStore,
    +      sessionKey,
    +      storePath,
    +      elevatedEnabled,
    +      elevatedAllowed,
    +      elevatedFailures,
    +      messageProviderKey,
    +      defaultProvider,
    +      defaultModel,
    +      aliasIndex,
    +      allowedModelKeys: modelState.allowedModelKeys,
    +      allowedModelCatalog: modelState.allowedModelCatalog,
    +      resetModelOverride: modelState.resetModelOverride,
    +      provider,
    +      model,
    +      initialModelLabel,
    +      formatModelSwitchEvent,
    +      agentCfg,
    +      modelState: {
    +        resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
    +        allowedModelKeys: modelState.allowedModelKeys,
    +        allowedModelCatalog: modelState.allowedModelCatalog,
    +        resetModelOverride: modelState.resetModelOverride,
    +      },
    +    });
    +    directiveAck = fastLane.directiveAck;
    +    provider = fastLane.provider;
    +    model = fastLane.model;
    +  }
    +
    +  const persisted = await persistInlineDirectives({
    +    directives,
    +    effectiveModelDirective,
    +    cfg,
    +    agentDir,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    elevatedEnabled,
    +    elevatedAllowed,
    +    defaultProvider,
    +    defaultModel,
    +    aliasIndex,
    +    allowedModelKeys: modelState.allowedModelKeys,
    +    provider,
    +    model,
    +    initialModelLabel,
    +    formatModelSwitchEvent,
    +    agentCfg,
    +  });
    +  provider = persisted.provider;
    +  model = persisted.model;
    +  contextTokens = persisted.contextTokens;
    +
    +  const perMessageQueueMode =
    +    directives.hasQueueDirective && !directives.queueReset
    +      ? directives.queueMode
    +      : undefined;
    +  const perMessageQueueOptions =
    +    directives.hasQueueDirective && !directives.queueReset
    +      ? {
    +          debounceMs: directives.debounceMs,
    +          cap: directives.cap,
    +          dropPolicy: directives.dropPolicy,
    +        }
    +      : undefined;
    +
    +  return {
    +    kind: "continue",
    +    directives,
    +    provider,
    +    model,
    +    contextTokens,
    +    directiveAck,
    +    perMessageQueueMode,
    +    perMessageQueueOptions,
    +  };
    +}
    
  • src/auto-reply/reply/get-reply-directives.ts+473 0 added
    @@ -0,0 +1,473 @@
    +import type { ModelAliasIndex } from "../../agents/model-selection.js";
    +import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import type { SessionEntry } from "../../config/sessions.js";
    +import {
    +  listChatCommands,
    +  shouldHandleTextCommands,
    +} from "../commands-registry.js";
    +import type { MsgContext, TemplateContext } from "../templating.js";
    +import type {
    +  ElevatedLevel,
    +  ReasoningLevel,
    +  ThinkLevel,
    +  VerboseLevel,
    +} from "../thinking.js";
    +import type { GetReplyOptions, ReplyPayload } from "../types.js";
    +import { resolveBlockStreamingChunking } from "./block-streaming.js";
    +import { buildCommandContext } from "./commands.js";
    +import {
    +  type InlineDirectives,
    +  parseInlineDirectives,
    +} from "./directive-handling.js";
    +import { applyInlineDirectiveOverrides } from "./get-reply-directives-apply.js";
    +import { clearInlineDirectives } from "./get-reply-directives-utils.js";
    +import {
    +  defaultGroupActivation,
    +  resolveGroupRequireMention,
    +} from "./groups.js";
    +import {
    +  CURRENT_MESSAGE_MARKER,
    +  stripMentions,
    +  stripStructuralPrefixes,
    +} from "./mentions.js";
    +import {
    +  createModelSelectionState,
    +  resolveContextTokens,
    +} from "./model-selection.js";
    +import {
    +  formatElevatedUnavailableMessage,
    +  resolveElevatedPermissions,
    +} from "./reply-elevated.js";
    +import { stripInlineStatus } from "./reply-inline.js";
    +import type { TypingController } from "./typing.js";
    +
    +type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
    +
    +export type ReplyDirectiveContinuation = {
    +  commandSource: string;
    +  command: ReturnType<typeof buildCommandContext>;
    +  allowTextCommands: boolean;
    +  directives: InlineDirectives;
    +  cleanedBody: string;
    +  messageProviderKey: string;
    +  elevatedEnabled: boolean;
    +  elevatedAllowed: boolean;
    +  elevatedFailures: Array<{ gate: string; key: string }>;
    +  defaultActivation: ReturnType<typeof defaultGroupActivation>;
    +  resolvedThinkLevel: ThinkLevel | undefined;
    +  resolvedVerboseLevel: VerboseLevel | undefined;
    +  resolvedReasoningLevel: ReasoningLevel;
    +  resolvedElevatedLevel: ElevatedLevel;
    +  blockStreamingEnabled: boolean;
    +  blockReplyChunking?: {
    +    minChars: number;
    +    maxChars: number;
    +    breakPreference: "paragraph" | "newline" | "sentence";
    +  };
    +  resolvedBlockStreamingBreak: "text_end" | "message_end";
    +  provider: string;
    +  model: string;
    +  modelState: Awaited<ReturnType<typeof createModelSelectionState>>;
    +  contextTokens: number;
    +  inlineStatusRequested: boolean;
    +  directiveAck?: ReplyPayload;
    +  perMessageQueueMode?: InlineDirectives["queueMode"];
    +  perMessageQueueOptions?: {
    +    debounceMs?: number;
    +    cap?: number;
    +    dropPolicy?: InlineDirectives["dropPolicy"];
    +  };
    +};
    +
    +export type ReplyDirectiveResult =
    +  | { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
    +  | { kind: "continue"; result: ReplyDirectiveContinuation };
    +
    +export async function resolveReplyDirectives(params: {
    +  ctx: MsgContext;
    +  cfg: ClawdbotConfig;
    +  agentId: string;
    +  agentDir: string;
    +  agentCfg: AgentDefaults;
    +  sessionCtx: TemplateContext;
    +  sessionEntry?: SessionEntry;
    +  sessionStore?: Record<string, SessionEntry>;
    +  sessionKey: string;
    +  storePath?: string;
    +  sessionScope: Parameters<
    +    typeof applyInlineDirectiveOverrides
    +  >[0]["sessionScope"];
    +  groupResolution: Parameters<
    +    typeof resolveGroupRequireMention
    +  >[0]["groupResolution"];
    +  isGroup: boolean;
    +  triggerBodyNormalized: string;
    +  commandAuthorized: boolean;
    +  defaultProvider: string;
    +  defaultModel: string;
    +  aliasIndex: ModelAliasIndex;
    +  provider: string;
    +  model: string;
    +  typing: TypingController;
    +  opts?: GetReplyOptions;
    +}): Promise<ReplyDirectiveResult> {
    +  const {
    +    ctx,
    +    cfg,
    +    agentId,
    +    agentCfg,
    +    agentDir,
    +    sessionCtx,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    sessionScope,
    +    groupResolution,
    +    isGroup,
    +    triggerBodyNormalized,
    +    commandAuthorized,
    +    defaultProvider,
    +    defaultModel,
    +    provider: initialProvider,
    +    model: initialModel,
    +    typing,
    +    opts,
    +  } = params;
    +  let provider = initialProvider;
    +  let model = initialModel;
    +
    +  // Prefer CommandBody/RawBody (clean message without structural context) for directive parsing.
    +  // Keep `Body`/`BodyStripped` as the best-available prompt text (may include context).
    +  const commandSource =
    +    sessionCtx.CommandBody ??
    +    sessionCtx.RawBody ??
    +    sessionCtx.BodyStripped ??
    +    sessionCtx.Body ??
    +    "";
    +  const command = buildCommandContext({
    +    ctx,
    +    cfg,
    +    agentId,
    +    sessionKey,
    +    isGroup,
    +    triggerBodyNormalized,
    +    commandAuthorized,
    +  });
    +  const allowTextCommands = shouldHandleTextCommands({
    +    cfg,
    +    surface: command.surface,
    +    commandSource: ctx.CommandSource,
    +  });
    +  const reservedCommands = new Set(
    +    listChatCommands().flatMap((cmd) =>
    +      cmd.textAliases.map((a) => a.replace(/^\//, "").toLowerCase()),
    +    ),
    +  );
    +  const configuredAliases = Object.values(cfg.agents?.defaults?.models ?? {})
    +    .map((entry) => entry.alias?.trim())
    +    .filter((alias): alias is string => Boolean(alias))
    +    .filter((alias) => !reservedCommands.has(alias.toLowerCase()));
    +  const allowStatusDirective = allowTextCommands && command.isAuthorizedSender;
    +  let parsedDirectives = parseInlineDirectives(commandSource, {
    +    modelAliases: configuredAliases,
    +    allowStatusDirective,
    +  });
    +  const hasInlineStatus =
    +    parsedDirectives.hasStatusDirective &&
    +    parsedDirectives.cleaned.trim().length > 0;
    +  if (hasInlineStatus) {
    +    parsedDirectives = {
    +      ...parsedDirectives,
    +      hasStatusDirective: false,
    +    };
    +  }
    +  if (
    +    isGroup &&
    +    ctx.WasMentioned !== true &&
    +    parsedDirectives.hasElevatedDirective
    +  ) {
    +    if (parsedDirectives.elevatedLevel !== "off") {
    +      parsedDirectives = {
    +        ...parsedDirectives,
    +        hasElevatedDirective: false,
    +        elevatedLevel: undefined,
    +        rawElevatedLevel: undefined,
    +      };
    +    }
    +  }
    +  const hasInlineDirective =
    +    parsedDirectives.hasThinkDirective ||
    +    parsedDirectives.hasVerboseDirective ||
    +    parsedDirectives.hasReasoningDirective ||
    +    parsedDirectives.hasElevatedDirective ||
    +    parsedDirectives.hasModelDirective ||
    +    parsedDirectives.hasQueueDirective;
    +  if (hasInlineDirective) {
    +    const stripped = stripStructuralPrefixes(parsedDirectives.cleaned);
    +    const noMentions = isGroup
    +      ? stripMentions(stripped, ctx, cfg, agentId)
    +      : stripped;
    +    if (noMentions.trim().length > 0) {
    +      const directiveOnlyCheck = parseInlineDirectives(noMentions, {
    +        modelAliases: configuredAliases,
    +      });
    +      if (directiveOnlyCheck.cleaned.trim().length > 0) {
    +        const allowInlineStatus =
    +          parsedDirectives.hasStatusDirective &&
    +          allowTextCommands &&
    +          command.isAuthorizedSender;
    +        parsedDirectives = allowInlineStatus
    +          ? {
    +              ...clearInlineDirectives(parsedDirectives.cleaned),
    +              hasStatusDirective: true,
    +            }
    +          : clearInlineDirectives(parsedDirectives.cleaned);
    +      }
    +    }
    +  }
    +  let directives = commandAuthorized
    +    ? parsedDirectives
    +    : {
    +        ...parsedDirectives,
    +        hasThinkDirective: false,
    +        hasVerboseDirective: false,
    +        hasReasoningDirective: false,
    +        hasStatusDirective: false,
    +        hasModelDirective: false,
    +        hasQueueDirective: false,
    +        queueReset: false,
    +      };
    +  const existingBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
    +  let cleanedBody = (() => {
    +    if (!existingBody) return parsedDirectives.cleaned;
    +    if (!sessionCtx.CommandBody && !sessionCtx.RawBody) {
    +      return parseInlineDirectives(existingBody, {
    +        modelAliases: configuredAliases,
    +        allowStatusDirective,
    +      }).cleaned;
    +    }
    +
    +    const markerIndex = existingBody.indexOf(CURRENT_MESSAGE_MARKER);
    +    if (markerIndex < 0) {
    +      return parseInlineDirectives(existingBody, {
    +        modelAliases: configuredAliases,
    +        allowStatusDirective,
    +      }).cleaned;
    +    }
    +
    +    const head = existingBody.slice(
    +      0,
    +      markerIndex + CURRENT_MESSAGE_MARKER.length,
    +    );
    +    const tail = existingBody.slice(
    +      markerIndex + CURRENT_MESSAGE_MARKER.length,
    +    );
    +    const cleanedTail = parseInlineDirectives(tail, {
    +      modelAliases: configuredAliases,
    +      allowStatusDirective,
    +    }).cleaned;
    +    return `${head}${cleanedTail}`;
    +  })();
    +
    +  if (allowStatusDirective) {
    +    cleanedBody = stripInlineStatus(cleanedBody).cleaned;
    +  }
    +
    +  sessionCtx.Body = cleanedBody;
    +  sessionCtx.BodyStripped = cleanedBody;
    +
    +  const messageProviderKey =
    +    sessionCtx.Provider?.trim().toLowerCase() ??
    +    ctx.Provider?.trim().toLowerCase() ??
    +    "";
    +  const elevated = resolveElevatedPermissions({
    +    cfg,
    +    agentId,
    +    ctx,
    +    provider: messageProviderKey,
    +  });
    +  const elevatedEnabled = elevated.enabled;
    +  const elevatedAllowed = elevated.allowed;
    +  const elevatedFailures = elevated.failures;
    +  if (
    +    directives.hasElevatedDirective &&
    +    (!elevatedEnabled || !elevatedAllowed)
    +  ) {
    +    typing.cleanup();
    +    const runtimeSandboxed = resolveSandboxRuntimeStatus({
    +      cfg,
    +      sessionKey: ctx.SessionKey,
    +    }).sandboxed;
    +    return {
    +      kind: "reply",
    +      reply: {
    +        text: formatElevatedUnavailableMessage({
    +          runtimeSandboxed,
    +          failures: elevatedFailures,
    +          sessionKey: ctx.SessionKey,
    +        }),
    +      },
    +    };
    +  }
    +
    +  const requireMention = resolveGroupRequireMention({
    +    cfg,
    +    ctx: sessionCtx,
    +    groupResolution,
    +  });
    +  const defaultActivation = defaultGroupActivation(requireMention);
    +  const resolvedThinkLevel =
    +    (directives.thinkLevel as ThinkLevel | undefined) ??
    +    (sessionEntry?.thinkingLevel as ThinkLevel | undefined) ??
    +    (agentCfg?.thinkingDefault as ThinkLevel | undefined);
    +
    +  const resolvedVerboseLevel =
    +    (directives.verboseLevel as VerboseLevel | undefined) ??
    +    (sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
    +    (agentCfg?.verboseDefault as VerboseLevel | undefined);
    +  const resolvedReasoningLevel: ReasoningLevel =
    +    (directives.reasoningLevel as ReasoningLevel | undefined) ??
    +    (sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ??
    +    "off";
    +  const resolvedElevatedLevel = elevatedAllowed
    +    ? ((directives.elevatedLevel as ElevatedLevel | undefined) ??
    +      (sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
    +      (agentCfg?.elevatedDefault as ElevatedLevel | undefined) ??
    +      "on")
    +    : "off";
    +  const resolvedBlockStreaming =
    +    opts?.disableBlockStreaming === true
    +      ? "off"
    +      : opts?.disableBlockStreaming === false
    +        ? "on"
    +        : agentCfg?.blockStreamingDefault === "on"
    +          ? "on"
    +          : "off";
    +  const resolvedBlockStreamingBreak: "text_end" | "message_end" =
    +    agentCfg?.blockStreamingBreak === "message_end"
    +      ? "message_end"
    +      : "text_end";
    +  const blockStreamingEnabled =
    +    resolvedBlockStreaming === "on" && opts?.disableBlockStreaming !== true;
    +  const blockReplyChunking = blockStreamingEnabled
    +    ? resolveBlockStreamingChunking(
    +        cfg,
    +        sessionCtx.Provider,
    +        sessionCtx.AccountId,
    +      )
    +    : undefined;
    +
    +  const modelState = await createModelSelectionState({
    +    cfg,
    +    agentCfg,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    defaultProvider,
    +    defaultModel,
    +    provider,
    +    model,
    +    hasModelDirective: directives.hasModelDirective,
    +  });
    +  provider = modelState.provider;
    +  model = modelState.model;
    +
    +  let contextTokens = resolveContextTokens({
    +    agentCfg,
    +    model,
    +  });
    +
    +  const initialModelLabel = `${provider}/${model}`;
    +  const formatModelSwitchEvent = (label: string, alias?: string) =>
    +    alias
    +      ? `Model switched to ${alias} (${label}).`
    +      : `Model switched to ${label}.`;
    +  const isModelListAlias =
    +    directives.hasModelDirective &&
    +    ["status", "list"].includes(
    +      directives.rawModelDirective?.trim().toLowerCase() ?? "",
    +    );
    +  const effectiveModelDirective = isModelListAlias
    +    ? undefined
    +    : directives.rawModelDirective;
    +
    +  const inlineStatusRequested =
    +    hasInlineStatus && allowTextCommands && command.isAuthorizedSender;
    +
    +  const applyResult = await applyInlineDirectiveOverrides({
    +    ctx,
    +    cfg,
    +    agentId,
    +    agentDir,
    +    agentCfg,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    sessionScope,
    +    isGroup,
    +    allowTextCommands,
    +    command,
    +    directives,
    +    messageProviderKey,
    +    elevatedEnabled,
    +    elevatedAllowed,
    +    elevatedFailures,
    +    defaultProvider,
    +    defaultModel,
    +    aliasIndex: params.aliasIndex,
    +    provider,
    +    model,
    +    modelState,
    +    initialModelLabel,
    +    formatModelSwitchEvent,
    +    resolvedElevatedLevel,
    +    defaultActivation: () => defaultActivation,
    +    contextTokens,
    +    effectiveModelDirective,
    +    typing,
    +  });
    +  if (applyResult.kind === "reply") {
    +    return { kind: "reply", reply: applyResult.reply };
    +  }
    +  directives = applyResult.directives;
    +  provider = applyResult.provider;
    +  model = applyResult.model;
    +  contextTokens = applyResult.contextTokens;
    +  const { directiveAck, perMessageQueueMode, perMessageQueueOptions } =
    +    applyResult;
    +
    +  return {
    +    kind: "continue",
    +    result: {
    +      commandSource,
    +      command,
    +      allowTextCommands,
    +      directives,
    +      cleanedBody,
    +      messageProviderKey,
    +      elevatedEnabled,
    +      elevatedAllowed,
    +      elevatedFailures,
    +      defaultActivation,
    +      resolvedThinkLevel,
    +      resolvedVerboseLevel,
    +      resolvedReasoningLevel,
    +      resolvedElevatedLevel,
    +      blockStreamingEnabled,
    +      blockReplyChunking,
    +      resolvedBlockStreamingBreak,
    +      provider,
    +      model,
    +      modelState,
    +      contextTokens,
    +      inlineStatusRequested,
    +      directiveAck,
    +      perMessageQueueMode,
    +      perMessageQueueOptions,
    +    },
    +  };
    +}
    
  • src/auto-reply/reply/get-reply-directives-utils.ts+33 0 added
    @@ -0,0 +1,33 @@
    +import type { InlineDirectives } from "./directive-handling.js";
    +
    +export function clearInlineDirectives(cleaned: string): InlineDirectives {
    +  return {
    +    cleaned,
    +    hasThinkDirective: false,
    +    thinkLevel: undefined,
    +    rawThinkLevel: undefined,
    +    hasVerboseDirective: false,
    +    verboseLevel: undefined,
    +    rawVerboseLevel: undefined,
    +    hasReasoningDirective: false,
    +    reasoningLevel: undefined,
    +    rawReasoningLevel: undefined,
    +    hasElevatedDirective: false,
    +    elevatedLevel: undefined,
    +    rawElevatedLevel: undefined,
    +    hasStatusDirective: false,
    +    hasModelDirective: false,
    +    rawModelDirective: undefined,
    +    hasQueueDirective: false,
    +    queueMode: undefined,
    +    queueReset: false,
    +    rawQueueMode: undefined,
    +    debounceMs: undefined,
    +    cap: undefined,
    +    dropPolicy: undefined,
    +    rawDebounce: undefined,
    +    rawCap: undefined,
    +    rawDrop: undefined,
    +    hasQueueOptions: false,
    +  };
    +}
    
  • src/auto-reply/reply/get-reply-inline-actions.ts+256 0 added
    @@ -0,0 +1,256 @@
    +import { getChannelDock } from "../../channels/dock.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import type { SessionEntry } from "../../config/sessions.js";
    +import type { MsgContext, TemplateContext } from "../templating.js";
    +import type {
    +  ElevatedLevel,
    +  ReasoningLevel,
    +  ThinkLevel,
    +  VerboseLevel,
    +} from "../thinking.js";
    +import type { GetReplyOptions, ReplyPayload } from "../types.js";
    +import { getAbortMemory } from "./abort.js";
    +import { buildStatusReply, handleCommands } from "./commands.js";
    +import type { InlineDirectives } from "./directive-handling.js";
    +import { isDirectiveOnly } from "./directive-handling.js";
    +import type { createModelSelectionState } from "./model-selection.js";
    +import { extractInlineSimpleCommand } from "./reply-inline.js";
    +import type { TypingController } from "./typing.js";
    +
    +export type InlineActionResult =
    +  | { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
    +  | {
    +      kind: "continue";
    +      directives: InlineDirectives;
    +      abortedLastRun: boolean;
    +    };
    +
    +export async function handleInlineActions(params: {
    +  ctx: MsgContext;
    +  sessionCtx: TemplateContext;
    +  cfg: ClawdbotConfig;
    +  agentId: string;
    +  sessionEntry?: SessionEntry;
    +  sessionStore?: Record<string, SessionEntry>;
    +  sessionKey: string;
    +  storePath?: string;
    +  sessionScope: Parameters<typeof buildStatusReply>[0]["sessionScope"];
    +  workspaceDir: string;
    +  isGroup: boolean;
    +  opts?: GetReplyOptions;
    +  typing: TypingController;
    +  allowTextCommands: boolean;
    +  inlineStatusRequested: boolean;
    +  command: Parameters<typeof handleCommands>[0]["command"];
    +  directives: InlineDirectives;
    +  cleanedBody: string;
    +  elevatedEnabled: boolean;
    +  elevatedAllowed: boolean;
    +  elevatedFailures: Array<{ gate: string; key: string }>;
    +  defaultActivation: Parameters<
    +    typeof buildStatusReply
    +  >[0]["defaultGroupActivation"];
    +  resolvedThinkLevel: ThinkLevel | undefined;
    +  resolvedVerboseLevel: VerboseLevel | undefined;
    +  resolvedReasoningLevel: ReasoningLevel;
    +  resolvedElevatedLevel: ElevatedLevel;
    +  resolveDefaultThinkingLevel: Awaited<
    +    ReturnType<typeof createModelSelectionState>
    +  >["resolveDefaultThinkingLevel"];
    +  provider: string;
    +  model: string;
    +  contextTokens: number;
    +  directiveAck?: ReplyPayload;
    +  abortedLastRun: boolean;
    +}): Promise<InlineActionResult> {
    +  const {
    +    ctx,
    +    sessionCtx,
    +    cfg,
    +    agentId,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    sessionScope,
    +    workspaceDir,
    +    isGroup,
    +    opts,
    +    typing,
    +    allowTextCommands,
    +    inlineStatusRequested,
    +    command,
    +    directives: initialDirectives,
    +    cleanedBody: initialCleanedBody,
    +    elevatedEnabled,
    +    elevatedAllowed,
    +    elevatedFailures,
    +    defaultActivation,
    +    resolvedThinkLevel,
    +    resolvedVerboseLevel,
    +    resolvedReasoningLevel,
    +    resolvedElevatedLevel,
    +    resolveDefaultThinkingLevel,
    +    provider,
    +    model,
    +    contextTokens,
    +    directiveAck,
    +    abortedLastRun: initialAbortedLastRun,
    +  } = params;
    +
    +  let directives = initialDirectives;
    +  let cleanedBody = initialCleanedBody;
    +
    +  const sendInlineReply = async (reply?: ReplyPayload) => {
    +    if (!reply) return;
    +    if (!opts?.onBlockReply) return;
    +    await opts.onBlockReply(reply);
    +  };
    +
    +  const inlineCommand =
    +    allowTextCommands && command.isAuthorizedSender
    +      ? extractInlineSimpleCommand(cleanedBody)
    +      : null;
    +  if (inlineCommand) {
    +    cleanedBody = inlineCommand.cleaned;
    +    sessionCtx.Body = cleanedBody;
    +    sessionCtx.BodyStripped = cleanedBody;
    +  }
    +
    +  const handleInlineStatus =
    +    !isDirectiveOnly({
    +      directives,
    +      cleanedBody: directives.cleaned,
    +      ctx,
    +      cfg,
    +      agentId,
    +      isGroup,
    +    }) && inlineStatusRequested;
    +  if (handleInlineStatus) {
    +    const inlineStatusReply = await buildStatusReply({
    +      cfg,
    +      command,
    +      sessionEntry,
    +      sessionKey,
    +      sessionScope,
    +      provider,
    +      model,
    +      contextTokens,
    +      resolvedThinkLevel,
    +      resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
    +      resolvedReasoningLevel,
    +      resolvedElevatedLevel,
    +      resolveDefaultThinkingLevel,
    +      isGroup,
    +      defaultGroupActivation: defaultActivation,
    +    });
    +    await sendInlineReply(inlineStatusReply);
    +    directives = { ...directives, hasStatusDirective: false };
    +  }
    +
    +  if (inlineCommand) {
    +    const inlineCommandContext = {
    +      ...command,
    +      rawBodyNormalized: inlineCommand.command,
    +      commandBodyNormalized: inlineCommand.command,
    +    };
    +    const inlineResult = await handleCommands({
    +      ctx,
    +      cfg,
    +      command: inlineCommandContext,
    +      agentId,
    +      directives,
    +      elevated: {
    +        enabled: elevatedEnabled,
    +        allowed: elevatedAllowed,
    +        failures: elevatedFailures,
    +      },
    +      sessionEntry,
    +      sessionStore,
    +      sessionKey,
    +      storePath,
    +      sessionScope,
    +      workspaceDir,
    +      defaultGroupActivation: defaultActivation,
    +      resolvedThinkLevel,
    +      resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
    +      resolvedReasoningLevel,
    +      resolvedElevatedLevel,
    +      resolveDefaultThinkingLevel,
    +      provider,
    +      model,
    +      contextTokens,
    +      isGroup,
    +    });
    +    if (inlineResult.reply) {
    +      if (!inlineCommand.cleaned) {
    +        typing.cleanup();
    +        return { kind: "reply", reply: inlineResult.reply };
    +      }
    +      await sendInlineReply(inlineResult.reply);
    +    }
    +  }
    +
    +  if (directiveAck) {
    +    await sendInlineReply(directiveAck);
    +  }
    +
    +  const isEmptyConfig = Object.keys(cfg).length === 0;
    +  const skipWhenConfigEmpty = command.channelId
    +    ? Boolean(getChannelDock(command.channelId)?.commands?.skipWhenConfigEmpty)
    +    : false;
    +  if (
    +    skipWhenConfigEmpty &&
    +    isEmptyConfig &&
    +    command.from &&
    +    command.to &&
    +    command.from !== command.to
    +  ) {
    +    typing.cleanup();
    +    return { kind: "reply", reply: undefined };
    +  }
    +
    +  let abortedLastRun = initialAbortedLastRun;
    +  if (!sessionEntry && command.abortKey) {
    +    abortedLastRun = getAbortMemory(command.abortKey) ?? false;
    +  }
    +
    +  const commandResult = await handleCommands({
    +    ctx,
    +    cfg,
    +    command,
    +    agentId,
    +    directives,
    +    elevated: {
    +      enabled: elevatedEnabled,
    +      allowed: elevatedAllowed,
    +      failures: elevatedFailures,
    +    },
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    sessionScope,
    +    workspaceDir,
    +    defaultGroupActivation: defaultActivation,
    +    resolvedThinkLevel,
    +    resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
    +    resolvedReasoningLevel,
    +    resolvedElevatedLevel,
    +    resolveDefaultThinkingLevel,
    +    provider,
    +    model,
    +    contextTokens,
    +    isGroup,
    +  });
    +  if (!commandResult.shouldContinue) {
    +    typing.cleanup();
    +    return { kind: "reply", reply: commandResult.reply };
    +  }
    +
    +  return {
    +    kind: "continue",
    +    directives,
    +    abortedLastRun,
    +  };
    +}
    
  • src/auto-reply/reply/get-reply-run.ts+425 0 added
    @@ -0,0 +1,425 @@
    +import crypto from "node:crypto";
    +import {
    +  abortEmbeddedPiRun,
    +  isEmbeddedPiRunActive,
    +  isEmbeddedPiRunStreaming,
    +  resolveEmbeddedSessionLane,
    +} from "../../agents/pi-embedded.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import {
    +  resolveSessionFilePath,
    +  type SessionEntry,
    +  saveSessionStore,
    +} from "../../config/sessions.js";
    +import { logVerbose } from "../../globals.js";
    +import { clearCommandLane, getQueueSize } from "../../process/command-queue.js";
    +import { normalizeMainKey } from "../../routing/session-key.js";
    +import { isReasoningTagProvider } from "../../utils/provider-utils.js";
    +import { hasControlCommand } from "../command-detection.js";
    +import { buildInboundMediaNote } from "../media-note.js";
    +import type { MsgContext, TemplateContext } from "../templating.js";
    +import {
    +  type ElevatedLevel,
    +  formatXHighModelHint,
    +  normalizeThinkLevel,
    +  type ReasoningLevel,
    +  supportsXHighThinking,
    +  type ThinkLevel,
    +  type VerboseLevel,
    +} from "../thinking.js";
    +import { SILENT_REPLY_TOKEN } from "../tokens.js";
    +import type { GetReplyOptions, ReplyPayload } from "../types.js";
    +import { runReplyAgent } from "./agent-runner.js";
    +import { applySessionHints } from "./body.js";
    +import type { buildCommandContext } from "./commands.js";
    +import type { InlineDirectives } from "./directive-handling.js";
    +import { buildGroupIntro } from "./groups.js";
    +import type { createModelSelectionState } from "./model-selection.js";
    +import { resolveQueueSettings } from "./queue.js";
    +import { ensureSkillSnapshot, prependSystemEvents } from "./session-updates.js";
    +import type { TypingController } from "./typing.js";
    +import { createTypingSignaler, resolveTypingMode } from "./typing-mode.js";
    +
    +type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
    +
    +const BARE_SESSION_RESET_PROMPT =
    +  "A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning.";
    +
    +type RunPreparedReplyParams = {
    +  ctx: MsgContext;
    +  sessionCtx: TemplateContext;
    +  cfg: ClawdbotConfig;
    +  agentId: string;
    +  agentDir: string;
    +  agentCfg: AgentDefaults;
    +  sessionCfg: ClawdbotConfig["session"];
    +  commandAuthorized: boolean;
    +  command: ReturnType<typeof buildCommandContext>;
    +  commandSource: string;
    +  allowTextCommands: boolean;
    +  directives: InlineDirectives;
    +  defaultActivation: Parameters<typeof buildGroupIntro>[0]["defaultActivation"];
    +  resolvedThinkLevel: ThinkLevel | undefined;
    +  resolvedVerboseLevel: VerboseLevel | undefined;
    +  resolvedReasoningLevel: ReasoningLevel;
    +  resolvedElevatedLevel: ElevatedLevel;
    +  elevatedEnabled: boolean;
    +  elevatedAllowed: boolean;
    +  blockStreamingEnabled: boolean;
    +  blockReplyChunking?: {
    +    minChars: number;
    +    maxChars: number;
    +    breakPreference: "paragraph" | "newline" | "sentence";
    +  };
    +  resolvedBlockStreamingBreak: "text_end" | "message_end";
    +  modelState: Awaited<ReturnType<typeof createModelSelectionState>>;
    +  provider: string;
    +  model: string;
    +  perMessageQueueMode?: InlineDirectives["queueMode"];
    +  perMessageQueueOptions?: {
    +    debounceMs?: number;
    +    cap?: number;
    +    dropPolicy?: InlineDirectives["dropPolicy"];
    +  };
    +  transcribedText?: string;
    +  typing: TypingController;
    +  opts?: GetReplyOptions;
    +  defaultModel: string;
    +  timeoutMs: number;
    +  isNewSession: boolean;
    +  systemSent: boolean;
    +  sessionEntry?: SessionEntry;
    +  sessionStore?: Record<string, SessionEntry>;
    +  sessionKey: string;
    +  sessionId?: string;
    +  storePath?: string;
    +  workspaceDir: string;
    +  abortedLastRun: boolean;
    +};
    +
    +export async function runPreparedReply(
    +  params: RunPreparedReplyParams,
    +): Promise<ReplyPayload | ReplyPayload[] | undefined> {
    +  const {
    +    ctx,
    +    sessionCtx,
    +    cfg,
    +    agentId,
    +    agentDir,
    +    agentCfg,
    +    sessionCfg,
    +    commandAuthorized,
    +    command,
    +    commandSource,
    +    allowTextCommands,
    +    directives,
    +    defaultActivation,
    +    elevatedEnabled,
    +    elevatedAllowed,
    +    blockStreamingEnabled,
    +    blockReplyChunking,
    +    resolvedBlockStreamingBreak,
    +    modelState,
    +    provider,
    +    model,
    +    perMessageQueueMode,
    +    perMessageQueueOptions,
    +    transcribedText,
    +    typing,
    +    opts,
    +    defaultModel,
    +    timeoutMs,
    +    isNewSession,
    +    systemSent,
    +    sessionKey,
    +    sessionId,
    +    storePath,
    +    workspaceDir,
    +    sessionStore,
    +  } = params;
    +  let {
    +    sessionEntry,
    +    resolvedThinkLevel,
    +    resolvedVerboseLevel,
    +    resolvedReasoningLevel,
    +    resolvedElevatedLevel,
    +    abortedLastRun,
    +  } = params;
    +  let currentSystemSent = systemSent;
    +
    +  const isFirstTurnInSession = isNewSession || !currentSystemSent;
    +  const isGroupChat = sessionCtx.ChatType === "group";
    +  const wasMentioned = ctx.WasMentioned === true;
    +  const isHeartbeat = opts?.isHeartbeat === true;
    +  const typingMode = resolveTypingMode({
    +    configured: sessionCfg?.typingMode ?? agentCfg?.typingMode,
    +    isGroupChat,
    +    wasMentioned,
    +    isHeartbeat,
    +  });
    +  const typingSignals = createTypingSignaler({
    +    typing,
    +    mode: typingMode,
    +    isHeartbeat,
    +  });
    +  const shouldInjectGroupIntro = Boolean(
    +    isGroupChat &&
    +      (isFirstTurnInSession || sessionEntry?.groupActivationNeedsSystemIntro),
    +  );
    +  const groupIntro = shouldInjectGroupIntro
    +    ? buildGroupIntro({
    +        cfg,
    +        sessionCtx,
    +        sessionEntry,
    +        defaultActivation,
    +        silentToken: SILENT_REPLY_TOKEN,
    +      })
    +    : "";
    +  const groupSystemPrompt = sessionCtx.GroupSystemPrompt?.trim() ?? "";
    +  const extraSystemPrompt = [groupIntro, groupSystemPrompt]
    +    .filter(Boolean)
    +    .join("\n\n");
    +  const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
    +  // Use CommandBody/RawBody for bare reset detection (clean message without structural context).
    +  const rawBodyTrimmed = (
    +    ctx.CommandBody ??
    +    ctx.RawBody ??
    +    ctx.Body ??
    +    ""
    +  ).trim();
    +  const baseBodyTrimmedRaw = baseBody.trim();
    +  if (
    +    allowTextCommands &&
    +    (!commandAuthorized || !command.isAuthorizedSender) &&
    +    !baseBodyTrimmedRaw &&
    +    hasControlCommand(commandSource, cfg)
    +  ) {
    +    typing.cleanup();
    +    return undefined;
    +  }
    +  const isBareSessionReset =
    +    isNewSession &&
    +    baseBodyTrimmedRaw.length === 0 &&
    +    rawBodyTrimmed.length > 0;
    +  const baseBodyFinal = isBareSessionReset
    +    ? BARE_SESSION_RESET_PROMPT
    +    : baseBody;
    +  const baseBodyTrimmed = baseBodyFinal.trim();
    +  if (!baseBodyTrimmed) {
    +    await typing.onReplyStart();
    +    logVerbose("Inbound body empty after normalization; skipping agent run");
    +    typing.cleanup();
    +    return {
    +      text: "I didn't receive any text in your message. Please resend or add a caption.",
    +    };
    +  }
    +  let prefixedBodyBase = await applySessionHints({
    +    baseBody: baseBodyFinal,
    +    abortedLastRun,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    abortKey: command.abortKey,
    +    messageId: sessionCtx.MessageSid,
    +  });
    +  const isGroupSession =
    +    sessionEntry?.chatType === "group" || sessionEntry?.chatType === "room";
    +  const isMainSession =
    +    !isGroupSession && sessionKey === normalizeMainKey(sessionCfg?.mainKey);
    +  prefixedBodyBase = await prependSystemEvents({
    +    cfg,
    +    sessionKey,
    +    isMainSession,
    +    isNewSession,
    +    prefixedBodyBase,
    +  });
    +  const threadStarterBody = ctx.ThreadStarterBody?.trim();
    +  const threadStarterNote =
    +    isNewSession && threadStarterBody
    +      ? `[Thread starter - for context]\n${threadStarterBody}`
    +      : undefined;
    +  const skillResult = await ensureSkillSnapshot({
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    sessionId,
    +    isFirstTurnInSession,
    +    workspaceDir,
    +    cfg,
    +    skillFilter: opts?.skillFilter,
    +  });
    +  sessionEntry = skillResult.sessionEntry ?? sessionEntry;
    +  currentSystemSent = skillResult.systemSent;
    +  const skillsSnapshot = skillResult.skillsSnapshot;
    +  const prefixedBody = transcribedText
    +    ? [threadStarterNote, prefixedBodyBase, `Transcript:\n${transcribedText}`]
    +        .filter(Boolean)
    +        .join("\n\n")
    +    : [threadStarterNote, prefixedBodyBase].filter(Boolean).join("\n\n");
    +  const mediaNote = buildInboundMediaNote(ctx);
    +  const mediaReplyHint = mediaNote
    +    ? "To send an image back, add a line like: MEDIA:https://example.com/image.jpg (no spaces). Keep caption in the text body."
    +    : undefined;
    +  let prefixedCommandBody = mediaNote
    +    ? [mediaNote, mediaReplyHint, prefixedBody ?? ""]
    +        .filter(Boolean)
    +        .join("\n")
    +        .trim()
    +    : prefixedBody;
    +  if (!resolvedThinkLevel && prefixedCommandBody) {
    +    const parts = prefixedCommandBody.split(/\s+/);
    +    const maybeLevel = normalizeThinkLevel(parts[0]);
    +    if (
    +      maybeLevel &&
    +      (maybeLevel !== "xhigh" || supportsXHighThinking(provider, model))
    +    ) {
    +      resolvedThinkLevel = maybeLevel;
    +      prefixedCommandBody = parts.slice(1).join(" ").trim();
    +    }
    +  }
    +  if (!resolvedThinkLevel) {
    +    resolvedThinkLevel = await modelState.resolveDefaultThinkingLevel();
    +  }
    +  if (
    +    resolvedThinkLevel === "xhigh" &&
    +    !supportsXHighThinking(provider, model)
    +  ) {
    +    const explicitThink =
    +      directives.hasThinkDirective && directives.thinkLevel !== undefined;
    +    if (explicitThink) {
    +      typing.cleanup();
    +      return {
    +        text: `Thinking level "xhigh" is only supported for ${formatXHighModelHint()}. Use /think high or switch to one of those models.`,
    +      };
    +    }
    +    resolvedThinkLevel = "high";
    +    if (
    +      sessionEntry &&
    +      sessionStore &&
    +      sessionKey &&
    +      sessionEntry.thinkingLevel === "xhigh"
    +    ) {
    +      sessionEntry.thinkingLevel = "high";
    +      sessionEntry.updatedAt = Date.now();
    +      sessionStore[sessionKey] = sessionEntry;
    +      if (storePath) {
    +        await saveSessionStore(storePath, sessionStore);
    +      }
    +    }
    +  }
    +  const sessionIdFinal = sessionId ?? crypto.randomUUID();
    +  const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
    +  const queueBodyBase = transcribedText
    +    ? [threadStarterNote, baseBodyFinal, `Transcript:\n${transcribedText}`]
    +        .filter(Boolean)
    +        .join("\n\n")
    +    : [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n");
    +  const queuedBody = mediaNote
    +    ? [mediaNote, mediaReplyHint, queueBodyBase]
    +        .filter(Boolean)
    +        .join("\n")
    +        .trim()
    +    : queueBodyBase;
    +  const resolvedQueue = resolveQueueSettings({
    +    cfg,
    +    channel: sessionCtx.Provider,
    +    sessionEntry,
    +    inlineMode: perMessageQueueMode,
    +    inlineOptions: perMessageQueueOptions,
    +  });
    +  const sessionLaneKey = resolveEmbeddedSessionLane(
    +    sessionKey ?? sessionIdFinal,
    +  );
    +  const laneSize = getQueueSize(sessionLaneKey);
    +  if (resolvedQueue.mode === "interrupt" && laneSize > 0) {
    +    const cleared = clearCommandLane(sessionLaneKey);
    +    const aborted = abortEmbeddedPiRun(sessionIdFinal);
    +    logVerbose(
    +      `Interrupting ${sessionLaneKey} (cleared ${cleared}, aborted=${aborted})`,
    +    );
    +  }
    +  const queueKey = sessionKey ?? sessionIdFinal;
    +  const isActive = isEmbeddedPiRunActive(sessionIdFinal);
    +  const isStreaming = isEmbeddedPiRunStreaming(sessionIdFinal);
    +  const shouldSteer =
    +    resolvedQueue.mode === "steer" || resolvedQueue.mode === "steer-backlog";
    +  const shouldFollowup =
    +    resolvedQueue.mode === "followup" ||
    +    resolvedQueue.mode === "collect" ||
    +    resolvedQueue.mode === "steer-backlog";
    +  const authProfileId = sessionEntry?.authProfileOverride;
    +  const followupRun = {
    +    prompt: queuedBody,
    +    messageId: sessionCtx.MessageSid,
    +    summaryLine: baseBodyTrimmedRaw,
    +    enqueuedAt: Date.now(),
    +    // Originating channel for reply routing.
    +    originatingChannel: ctx.OriginatingChannel,
    +    originatingTo: ctx.OriginatingTo,
    +    originatingAccountId: ctx.AccountId,
    +    originatingThreadId: ctx.MessageThreadId,
    +    run: {
    +      agentId,
    +      agentDir,
    +      sessionId: sessionIdFinal,
    +      sessionKey,
    +      messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
    +      agentAccountId: sessionCtx.AccountId,
    +      sessionFile,
    +      workspaceDir,
    +      config: cfg,
    +      skillsSnapshot,
    +      provider,
    +      model,
    +      authProfileId,
    +      thinkLevel: resolvedThinkLevel,
    +      verboseLevel: resolvedVerboseLevel,
    +      reasoningLevel: resolvedReasoningLevel,
    +      elevatedLevel: resolvedElevatedLevel,
    +      bashElevated: {
    +        enabled: elevatedEnabled,
    +        allowed: elevatedAllowed,
    +        defaultLevel: resolvedElevatedLevel ?? "off",
    +      },
    +      timeoutMs,
    +      blockReplyBreak: resolvedBlockStreamingBreak,
    +      ownerNumbers:
    +        command.ownerList.length > 0 ? command.ownerList : undefined,
    +      extraSystemPrompt: extraSystemPrompt || undefined,
    +      ...(isReasoningTagProvider(provider) ? { enforceFinalTag: true } : {}),
    +    },
    +  };
    +
    +  if (typingSignals.shouldStartImmediately) {
    +    await typingSignals.signalRunStart();
    +  }
    +
    +  return runReplyAgent({
    +    commandBody: prefixedCommandBody,
    +    followupRun,
    +    queueKey,
    +    resolvedQueue,
    +    shouldSteer,
    +    shouldFollowup,
    +    isActive,
    +    isStreaming,
    +    opts,
    +    typing,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    defaultModel,
    +    agentCfgContextTokens: agentCfg?.contextTokens,
    +    resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
    +    isNewSession,
    +    blockStreamingEnabled,
    +    blockReplyChunking,
    +    resolvedBlockStreamingBreak,
    +    sessionCtx,
    +    shouldInjectGroupIntro,
    +    typingMode,
    +  });
    +}
    
  • src/auto-reply/reply/get-reply.ts+272 0 added
    @@ -0,0 +1,272 @@
    +import {
    +  resolveAgentDir,
    +  resolveAgentWorkspaceDir,
    +  resolveSessionAgentId,
    +} from "../../agents/agent-scope.js";
    +import { resolveModelRefFromString } from "../../agents/model-selection.js";
    +import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
    +import {
    +  DEFAULT_AGENT_WORKSPACE_DIR,
    +  ensureAgentWorkspace,
    +} from "../../agents/workspace.js";
    +import { type ClawdbotConfig, loadConfig } from "../../config/config.js";
    +import { logVerbose } from "../../globals.js";
    +import { defaultRuntime } from "../../runtime.js";
    +import { resolveCommandAuthorization } from "../command-auth.js";
    +import type { MsgContext } from "../templating.js";
    +import { SILENT_REPLY_TOKEN } from "../tokens.js";
    +import {
    +  hasAudioTranscriptionConfig,
    +  isAudio,
    +  transcribeInboundAudio,
    +} from "../transcription.js";
    +import type { GetReplyOptions, ReplyPayload } from "../types.js";
    +import { resolveDefaultModel } from "./directive-handling.js";
    +import { resolveReplyDirectives } from "./get-reply-directives.js";
    +import { handleInlineActions } from "./get-reply-inline-actions.js";
    +import { runPreparedReply } from "./get-reply-run.js";
    +import { initSessionState } from "./session.js";
    +import { stageSandboxMedia } from "./stage-sandbox-media.js";
    +import { createTypingController } from "./typing.js";
    +
    +export async function getReplyFromConfig(
    +  ctx: MsgContext,
    +  opts?: GetReplyOptions,
    +  configOverride?: ClawdbotConfig,
    +): Promise<ReplyPayload | ReplyPayload[] | undefined> {
    +  const cfg = configOverride ?? loadConfig();
    +  const agentId = resolveSessionAgentId({
    +    sessionKey: ctx.SessionKey,
    +    config: cfg,
    +  });
    +  const agentCfg = cfg.agents?.defaults;
    +  const sessionCfg = cfg.session;
    +  const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
    +    cfg,
    +    agentId,
    +  });
    +  let provider = defaultProvider;
    +  let model = defaultModel;
    +  if (opts?.isHeartbeat) {
    +    const heartbeatRaw = agentCfg?.heartbeat?.model?.trim() ?? "";
    +    const heartbeatRef = heartbeatRaw
    +      ? resolveModelRefFromString({
    +          raw: heartbeatRaw,
    +          defaultProvider,
    +          aliasIndex,
    +        })
    +      : null;
    +    if (heartbeatRef) {
    +      provider = heartbeatRef.ref.provider;
    +      model = heartbeatRef.ref.model;
    +    }
    +  }
    +
    +  const workspaceDirRaw =
    +    resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR;
    +  const workspace = await ensureAgentWorkspace({
    +    dir: workspaceDirRaw,
    +    ensureBootstrapFiles: !agentCfg?.skipBootstrap,
    +  });
    +  const workspaceDir = workspace.dir;
    +  const agentDir = resolveAgentDir(cfg, agentId);
    +  const timeoutMs = resolveAgentTimeoutMs({ cfg });
    +  const configuredTypingSeconds =
    +    agentCfg?.typingIntervalSeconds ?? sessionCfg?.typingIntervalSeconds;
    +  const typingIntervalSeconds =
    +    typeof configuredTypingSeconds === "number" ? configuredTypingSeconds : 6;
    +  const typing = createTypingController({
    +    onReplyStart: opts?.onReplyStart,
    +    typingIntervalSeconds,
    +    silentToken: SILENT_REPLY_TOKEN,
    +    log: defaultRuntime.log,
    +  });
    +  opts?.onTypingController?.(typing);
    +
    +  let transcribedText: string | undefined;
    +  if (hasAudioTranscriptionConfig(cfg) && isAudio(ctx.MediaType)) {
    +    const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime);
    +    if (transcribed?.text) {
    +      transcribedText = transcribed.text;
    +      ctx.Body = transcribed.text;
    +      ctx.Transcript = transcribed.text;
    +      logVerbose("Replaced Body with audio transcript for reply flow");
    +    }
    +  }
    +
    +  const commandAuthorized = ctx.CommandAuthorized ?? true;
    +  resolveCommandAuthorization({
    +    ctx,
    +    cfg,
    +    commandAuthorized,
    +  });
    +  const sessionState = await initSessionState({
    +    ctx,
    +    cfg,
    +    commandAuthorized,
    +  });
    +  let {
    +    sessionCtx,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    sessionId,
    +    isNewSession,
    +    systemSent,
    +    abortedLastRun,
    +    storePath,
    +    sessionScope,
    +    groupResolution,
    +    isGroup,
    +    triggerBodyNormalized,
    +  } = sessionState;
    +
    +  const directiveResult = await resolveReplyDirectives({
    +    ctx,
    +    cfg,
    +    agentId,
    +    agentDir,
    +    agentCfg,
    +    sessionCtx,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    sessionScope,
    +    groupResolution,
    +    isGroup,
    +    triggerBodyNormalized,
    +    commandAuthorized,
    +    defaultProvider,
    +    defaultModel,
    +    aliasIndex,
    +    provider,
    +    model,
    +    typing,
    +    opts,
    +  });
    +  if (directiveResult.kind === "reply") {
    +    return directiveResult.reply;
    +  }
    +
    +  let {
    +    commandSource,
    +    command,
    +    allowTextCommands,
    +    directives,
    +    cleanedBody,
    +    elevatedEnabled,
    +    elevatedAllowed,
    +    elevatedFailures,
    +    defaultActivation,
    +    resolvedThinkLevel,
    +    resolvedVerboseLevel,
    +    resolvedReasoningLevel,
    +    resolvedElevatedLevel,
    +    blockStreamingEnabled,
    +    blockReplyChunking,
    +    resolvedBlockStreamingBreak,
    +    provider: resolvedProvider,
    +    model: resolvedModel,
    +    modelState,
    +    contextTokens,
    +    inlineStatusRequested,
    +    directiveAck,
    +    perMessageQueueMode,
    +    perMessageQueueOptions,
    +  } = directiveResult.result;
    +  provider = resolvedProvider;
    +  model = resolvedModel;
    +
    +  const inlineActionResult = await handleInlineActions({
    +    ctx,
    +    sessionCtx,
    +    cfg,
    +    agentId,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    storePath,
    +    sessionScope,
    +    workspaceDir,
    +    isGroup,
    +    opts,
    +    typing,
    +    allowTextCommands,
    +    inlineStatusRequested,
    +    command,
    +    directives,
    +    cleanedBody,
    +    elevatedEnabled,
    +    elevatedAllowed,
    +    elevatedFailures,
    +    defaultActivation: () => defaultActivation,
    +    resolvedThinkLevel,
    +    resolvedVerboseLevel,
    +    resolvedReasoningLevel,
    +    resolvedElevatedLevel,
    +    resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
    +    provider,
    +    model,
    +    contextTokens,
    +    directiveAck,
    +    abortedLastRun,
    +  });
    +  if (inlineActionResult.kind === "reply") {
    +    return inlineActionResult.reply;
    +  }
    +  directives = inlineActionResult.directives;
    +  abortedLastRun = inlineActionResult.abortedLastRun ?? abortedLastRun;
    +
    +  await stageSandboxMedia({
    +    ctx,
    +    sessionCtx,
    +    cfg,
    +    sessionKey,
    +    workspaceDir,
    +  });
    +
    +  return runPreparedReply({
    +    ctx,
    +    sessionCtx,
    +    cfg,
    +    agentId,
    +    agentDir,
    +    agentCfg,
    +    sessionCfg,
    +    commandAuthorized,
    +    command,
    +    commandSource,
    +    allowTextCommands,
    +    directives,
    +    defaultActivation,
    +    resolvedThinkLevel,
    +    resolvedVerboseLevel,
    +    resolvedReasoningLevel,
    +    resolvedElevatedLevel,
    +    elevatedEnabled,
    +    elevatedAllowed,
    +    blockStreamingEnabled,
    +    blockReplyChunking,
    +    resolvedBlockStreamingBreak,
    +    modelState,
    +    provider,
    +    model,
    +    perMessageQueueMode,
    +    perMessageQueueOptions,
    +    transcribedText,
    +    typing,
    +    opts,
    +    defaultModel,
    +    timeoutMs,
    +    isNewSession,
    +    systemSent,
    +    sessionEntry,
    +    sessionStore,
    +    sessionKey,
    +    sessionId,
    +    storePath,
    +    workspaceDir,
    +    abortedLastRun,
    +  });
    +}
    
  • src/auto-reply/reply/reply-elevated.ts+206 0 added
    @@ -0,0 +1,206 @@
    +import { resolveAgentConfig } from "../../agents/agent-scope.js";
    +import { getChannelDock } from "../../channels/dock.js";
    +import {
    +  CHAT_CHANNEL_ORDER,
    +  normalizeChannelId,
    +} from "../../channels/registry.js";
    +import type {
    +  AgentElevatedAllowFromConfig,
    +  ClawdbotConfig,
    +} from "../../config/config.js";
    +import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
    +import type { MsgContext } from "../templating.js";
    +
    +function normalizeAllowToken(value?: string) {
    +  if (!value) return "";
    +  return value.trim().toLowerCase();
    +}
    +
    +function slugAllowToken(value?: string) {
    +  if (!value) return "";
    +  let text = value.trim().toLowerCase();
    +  if (!text) return "";
    +  text = text.replace(/^[@#]+/, "");
    +  text = text.replace(/[\s_]+/g, "-");
    +  text = text.replace(/[^a-z0-9-]+/g, "-");
    +  return text.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
    +}
    +
    +const SENDER_PREFIXES = [
    +  ...CHAT_CHANNEL_ORDER,
    +  INTERNAL_MESSAGE_CHANNEL,
    +  "user",
    +  "group",
    +  "channel",
    +];
    +const SENDER_PREFIX_RE = new RegExp(`^(${SENDER_PREFIXES.join("|")}):`, "i");
    +
    +function stripSenderPrefix(value?: string) {
    +  if (!value) return "";
    +  const trimmed = value.trim();
    +  return trimmed.replace(SENDER_PREFIX_RE, "");
    +}
    +
    +function resolveElevatedAllowList(
    +  allowFrom: AgentElevatedAllowFromConfig | undefined,
    +  provider: string,
    +  fallbackAllowFrom?: Array<string | number>,
    +): Array<string | number> | undefined {
    +  if (!allowFrom) return fallbackAllowFrom;
    +  const value = allowFrom[provider];
    +  return Array.isArray(value) ? value : fallbackAllowFrom;
    +}
    +
    +function isApprovedElevatedSender(params: {
    +  provider: string;
    +  ctx: MsgContext;
    +  allowFrom?: AgentElevatedAllowFromConfig;
    +  fallbackAllowFrom?: Array<string | number>;
    +}): boolean {
    +  const rawAllow = resolveElevatedAllowList(
    +    params.allowFrom,
    +    params.provider,
    +    params.fallbackAllowFrom,
    +  );
    +  if (!rawAllow || rawAllow.length === 0) return false;
    +
    +  const allowTokens = rawAllow
    +    .map((entry) => String(entry).trim())
    +    .filter(Boolean);
    +  if (allowTokens.length === 0) return false;
    +  if (allowTokens.some((entry) => entry === "*")) return true;
    +
    +  const tokens = new Set<string>();
    +  const addToken = (value?: string) => {
    +    if (!value) return;
    +    const trimmed = value.trim();
    +    if (!trimmed) return;
    +    tokens.add(trimmed);
    +    const normalized = normalizeAllowToken(trimmed);
    +    if (normalized) tokens.add(normalized);
    +    const slugged = slugAllowToken(trimmed);
    +    if (slugged) tokens.add(slugged);
    +  };
    +
    +  addToken(params.ctx.SenderName);
    +  addToken(params.ctx.SenderUsername);
    +  addToken(params.ctx.SenderTag);
    +  addToken(params.ctx.SenderE164);
    +  addToken(params.ctx.From);
    +  addToken(stripSenderPrefix(params.ctx.From));
    +  addToken(params.ctx.To);
    +  addToken(stripSenderPrefix(params.ctx.To));
    +
    +  for (const rawEntry of allowTokens) {
    +    const entry = rawEntry.trim();
    +    if (!entry) continue;
    +    const stripped = stripSenderPrefix(entry);
    +    if (tokens.has(entry) || tokens.has(stripped)) return true;
    +    const normalized = normalizeAllowToken(stripped);
    +    if (normalized && tokens.has(normalized)) return true;
    +    const slugged = slugAllowToken(stripped);
    +    if (slugged && tokens.has(slugged)) return true;
    +  }
    +
    +  return false;
    +}
    +
    +export function resolveElevatedPermissions(params: {
    +  cfg: ClawdbotConfig;
    +  agentId: string;
    +  ctx: MsgContext;
    +  provider: string;
    +}): {
    +  enabled: boolean;
    +  allowed: boolean;
    +  failures: Array<{ gate: string; key: string }>;
    +} {
    +  const globalConfig = params.cfg.tools?.elevated;
    +  const agentConfig = resolveAgentConfig(params.cfg, params.agentId)?.tools
    +    ?.elevated;
    +  const globalEnabled = globalConfig?.enabled !== false;
    +  const agentEnabled = agentConfig?.enabled !== false;
    +  const enabled = globalEnabled && agentEnabled;
    +  const failures: Array<{ gate: string; key: string }> = [];
    +  if (!globalEnabled)
    +    failures.push({ gate: "enabled", key: "tools.elevated.enabled" });
    +  if (!agentEnabled)
    +    failures.push({
    +      gate: "enabled",
    +      key: "agents.list[].tools.elevated.enabled",
    +    });
    +  if (!enabled) return { enabled, allowed: false, failures };
    +  if (!params.provider) {
    +    failures.push({ gate: "provider", key: "ctx.Provider" });
    +    return { enabled, allowed: false, failures };
    +  }
    +
    +  const normalizedProvider = normalizeChannelId(params.provider);
    +  const dockFallbackAllowFrom = normalizedProvider
    +    ? getChannelDock(normalizedProvider)?.elevated?.allowFromFallback?.({
    +        cfg: params.cfg,
    +        accountId: params.ctx.AccountId,
    +      })
    +    : undefined;
    +  const fallbackAllowFrom = dockFallbackAllowFrom;
    +  const globalAllowed = isApprovedElevatedSender({
    +    provider: params.provider,
    +    ctx: params.ctx,
    +    allowFrom: globalConfig?.allowFrom,
    +    fallbackAllowFrom,
    +  });
    +  if (!globalAllowed) {
    +    failures.push({
    +      gate: "allowFrom",
    +      key: `tools.elevated.allowFrom.${params.provider}`,
    +    });
    +    return { enabled, allowed: false, failures };
    +  }
    +
    +  const agentAllowed = agentConfig?.allowFrom
    +    ? isApprovedElevatedSender({
    +        provider: params.provider,
    +        ctx: params.ctx,
    +        allowFrom: agentConfig.allowFrom,
    +        fallbackAllowFrom,
    +      })
    +    : true;
    +  if (!agentAllowed) {
    +    failures.push({
    +      gate: "allowFrom",
    +      key: `agents.list[].tools.elevated.allowFrom.${params.provider}`,
    +    });
    +  }
    +  return { enabled, allowed: globalAllowed && agentAllowed, failures };
    +}
    +
    +export function formatElevatedUnavailableMessage(params: {
    +  runtimeSandboxed: boolean;
    +  failures: Array<{ gate: string; key: string }>;
    +  sessionKey?: string;
    +}): string {
    +  const lines: string[] = [];
    +  lines.push(
    +    `elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
    +  );
    +  if (params.failures.length > 0) {
    +    lines.push(
    +      `Failing gates: ${params.failures
    +        .map((f) => `${f.gate} (${f.key})`)
    +        .join(", ")}`,
    +    );
    +  } else {
    +    lines.push(
    +      "Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>).",
    +    );
    +  }
    +  lines.push("Fix-it keys:");
    +  lines.push("- tools.elevated.enabled");
    +  lines.push("- tools.elevated.allowFrom.<provider>");
    +  lines.push("- agents.list[].tools.elevated.enabled");
    +  lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
    +  if (params.sessionKey) {
    +    lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
    +  }
    +  return lines.join("\n");
    +}
    
  • src/auto-reply/reply/reply-inline.ts+37 0 added
    @@ -0,0 +1,37 @@
    +const INLINE_SIMPLE_COMMAND_ALIASES = new Map<string, string>([
    +  ["/help", "/help"],
    +  ["/commands", "/commands"],
    +  ["/whoami", "/whoami"],
    +  ["/id", "/whoami"],
    +]);
    +const INLINE_SIMPLE_COMMAND_RE =
    +  /(?:^|\s)\/(help|commands|whoami|id)(?=$|\s|:)/i;
    +
    +const INLINE_STATUS_RE = /(?:^|\s)\/(?:status|usage)(?=$|\s|:)(?:\s*:\s*)?/gi;
    +
    +export function extractInlineSimpleCommand(body?: string): {
    +  command: string;
    +  cleaned: string;
    +} | null {
    +  if (!body) return null;
    +  const match = body.match(INLINE_SIMPLE_COMMAND_RE);
    +  if (!match || match.index === undefined) return null;
    +  const alias = `/${match[1].toLowerCase()}`;
    +  const command = INLINE_SIMPLE_COMMAND_ALIASES.get(alias);
    +  if (!command) return null;
    +  const cleaned = body.replace(match[0], " ").replace(/\s+/g, " ").trim();
    +  return { command, cleaned };
    +}
    +
    +export function stripInlineStatus(body: string): {
    +  cleaned: string;
    +  didStrip: boolean;
    +} {
    +  const trimmed = body.trim();
    +  if (!trimmed) return { cleaned: "", didStrip: false };
    +  const cleaned = trimmed
    +    .replace(INLINE_STATUS_RE, " ")
    +    .replace(/\s+/g, " ")
    +    .trim();
    +  return { cleaned, didStrip: cleaned !== trimmed };
    +}
    
  • src/auto-reply/reply/stage-sandbox-media.ts+118 0 added
    @@ -0,0 +1,118 @@
    +import fs from "node:fs/promises";
    +import path from "node:path";
    +import { fileURLToPath } from "node:url";
    +import { ensureSandboxWorkspaceForSession } from "../../agents/sandbox.js";
    +import type { ClawdbotConfig } from "../../config/config.js";
    +import { logVerbose } from "../../globals.js";
    +import type { MsgContext, TemplateContext } from "../templating.js";
    +
    +export async function stageSandboxMedia(params: {
    +  ctx: MsgContext;
    +  sessionCtx: TemplateContext;
    +  cfg: ClawdbotConfig;
    +  sessionKey?: string;
    +  workspaceDir: string;
    +}) {
    +  const { ctx, sessionCtx, cfg, sessionKey, workspaceDir } = params;
    +  const hasPathsArray =
    +    Array.isArray(ctx.MediaPaths) && ctx.MediaPaths.length > 0;
    +  const pathsFromArray = Array.isArray(ctx.MediaPaths)
    +    ? ctx.MediaPaths
    +    : undefined;
    +  const rawPaths =
    +    pathsFromArray && pathsFromArray.length > 0
    +      ? pathsFromArray
    +      : ctx.MediaPath?.trim()
    +        ? [ctx.MediaPath.trim()]
    +        : [];
    +  if (rawPaths.length === 0 || !sessionKey) return;
    +
    +  const sandbox = await ensureSandboxWorkspaceForSession({
    +    config: cfg,
    +    sessionKey,
    +    workspaceDir,
    +  });
    +  if (!sandbox) return;
    +
    +  const resolveAbsolutePath = (value: string): string | null => {
    +    let resolved = value.trim();
    +    if (!resolved) return null;
    +    if (resolved.startsWith("file://")) {
    +      try {
    +        resolved = fileURLToPath(resolved);
    +      } catch {
    +        return null;
    +      }
    +    }
    +    if (!path.isAbsolute(resolved)) return null;
    +    return resolved;
    +  };
    +
    +  try {
    +    const destDir = path.join(sandbox.workspaceDir, "media", "inbound");
    +    await fs.mkdir(destDir, { recursive: true });
    +
    +    const usedNames = new Set<string>();
    +    const staged = new Map<string, string>(); // absolute source -> relative sandbox path
    +
    +    for (const raw of rawPaths) {
    +      const source = resolveAbsolutePath(raw);
    +      if (!source) continue;
    +      if (staged.has(source)) continue;
    +
    +      const baseName = path.basename(source);
    +      if (!baseName) continue;
    +      const parsed = path.parse(baseName);
    +      let fileName = baseName;
    +      let suffix = 1;
    +      while (usedNames.has(fileName)) {
    +        fileName = `${parsed.name}-${suffix}${parsed.ext}`;
    +        suffix += 1;
    +      }
    +      usedNames.add(fileName);
    +
    +      const dest = path.join(destDir, fileName);
    +      await fs.copyFile(source, dest);
    +      const relative = path.posix.join("media", "inbound", fileName);
    +      staged.set(source, relative);
    +    }
    +
    +    const rewriteIfStaged = (value: string | undefined): string | undefined => {
    +      const raw = value?.trim();
    +      if (!raw) return value;
    +      const abs = resolveAbsolutePath(raw);
    +      if (!abs) return value;
    +      const mapped = staged.get(abs);
    +      return mapped ?? value;
    +    };
    +
    +    const nextMediaPaths = hasPathsArray
    +      ? rawPaths.map((p) => rewriteIfStaged(p) ?? p)
    +      : undefined;
    +    if (nextMediaPaths) {
    +      ctx.MediaPaths = nextMediaPaths;
    +      sessionCtx.MediaPaths = nextMediaPaths;
    +      ctx.MediaPath = nextMediaPaths[0];
    +      sessionCtx.MediaPath = nextMediaPaths[0];
    +    } else {
    +      const rewritten = rewriteIfStaged(ctx.MediaPath);
    +      if (rewritten && rewritten !== ctx.MediaPath) {
    +        ctx.MediaPath = rewritten;
    +        sessionCtx.MediaPath = rewritten;
    +      }
    +    }
    +
    +    if (Array.isArray(ctx.MediaUrls) && ctx.MediaUrls.length > 0) {
    +      const nextUrls = ctx.MediaUrls.map((u) => rewriteIfStaged(u) ?? u);
    +      ctx.MediaUrls = nextUrls;
    +      sessionCtx.MediaUrls = nextUrls;
    +    }
    +    const rewrittenUrl = rewriteIfStaged(ctx.MediaUrl);
    +    if (rewrittenUrl && rewrittenUrl !== ctx.MediaUrl) {
    +      ctx.MediaUrl = rewrittenUrl;
    +      sessionCtx.MediaUrl = rewrittenUrl;
    +    }
    +  } catch (err) {
    +    logVerbose(`Failed to stage inbound media for sandbox: ${String(err)}`);
    +  }
    +}
    
  • src/auto-reply/reply.ts+1 1456 modified
    @@ -1,1465 +1,10 @@
    -import crypto from "node:crypto";
    -import fs from "node:fs/promises";
    -import path from "node:path";
    -import { fileURLToPath } from "node:url";
    -import {
    -  resolveAgentConfig,
    -  resolveAgentDir,
    -  resolveAgentWorkspaceDir,
    -  resolveSessionAgentId,
    -} from "../agents/agent-scope.js";
    -import { resolveModelRefFromString } from "../agents/model-selection.js";
    -import {
    -  abortEmbeddedPiRun,
    -  isEmbeddedPiRunActive,
    -  isEmbeddedPiRunStreaming,
    -  resolveEmbeddedSessionLane,
    -} from "../agents/pi-embedded.js";
    -import {
    -  ensureSandboxWorkspaceForSession,
    -  resolveSandboxRuntimeStatus,
    -} from "../agents/sandbox.js";
    -import { resolveAgentTimeoutMs } from "../agents/timeout.js";
    -import {
    -  DEFAULT_AGENT_WORKSPACE_DIR,
    -  ensureAgentWorkspace,
    -} from "../agents/workspace.js";
    -import { getChannelDock } from "../channels/dock.js";
    -import {
    -  CHAT_CHANNEL_ORDER,
    -  normalizeChannelId,
    -} from "../channels/registry.js";
    -import {
    -  type AgentElevatedAllowFromConfig,
    -  type ClawdbotConfig,
    -  loadConfig,
    -} from "../config/config.js";
    -import {
    -  resolveSessionFilePath,
    -  saveSessionStore,
    -} from "../config/sessions.js";
    -import { logVerbose } from "../globals.js";
    -import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
    -import { normalizeMainKey } from "../routing/session-key.js";
    -import { defaultRuntime } from "../runtime.js";
    -import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
    -import { isReasoningTagProvider } from "../utils/provider-utils.js";
    -import { resolveCommandAuthorization } from "./command-auth.js";
    -import { hasControlCommand } from "./command-detection.js";
    -import {
    -  listChatCommands,
    -  shouldHandleTextCommands,
    -} from "./commands-registry.js";
    -import { buildInboundMediaNote } from "./media-note.js";
    -import { getAbortMemory } from "./reply/abort.js";
    -import { runReplyAgent } from "./reply/agent-runner.js";
    -import { resolveBlockStreamingChunking } from "./reply/block-streaming.js";
    -import { applySessionHints } from "./reply/body.js";
    -import {
    -  buildCommandContext,
    -  buildStatusReply,
    -  handleCommands,
    -} from "./reply/commands.js";
    -import {
    -  applyInlineDirectivesFastLane,
    -  handleDirectiveOnly,
    -  type InlineDirectives,
    -  isDirectiveOnly,
    -  parseInlineDirectives,
    -  persistInlineDirectives,
    -  resolveDefaultModel,
    -} from "./reply/directive-handling.js";
    -import {
    -  buildGroupIntro,
    -  defaultGroupActivation,
    -  resolveGroupRequireMention,
    -} from "./reply/groups.js";
    -import {
    -  CURRENT_MESSAGE_MARKER,
    -  stripMentions,
    -  stripStructuralPrefixes,
    -} from "./reply/mentions.js";
    -import {
    -  createModelSelectionState,
    -  resolveContextTokens,
    -} from "./reply/model-selection.js";
    -import { resolveQueueSettings } from "./reply/queue.js";
    -import { initSessionState } from "./reply/session.js";
    -import {
    -  ensureSkillSnapshot,
    -  prependSystemEvents,
    -} from "./reply/session-updates.js";
    -import { createTypingController } from "./reply/typing.js";
    -import {
    -  createTypingSignaler,
    -  resolveTypingMode,
    -} from "./reply/typing-mode.js";
    -import type { MsgContext, TemplateContext } from "./templating.js";
    -import {
    -  type ElevatedLevel,
    -  formatXHighModelHint,
    -  normalizeThinkLevel,
    -  type ReasoningLevel,
    -  supportsXHighThinking,
    -  type ThinkLevel,
    -  type VerboseLevel,
    -} from "./thinking.js";
    -import { SILENT_REPLY_TOKEN } from "./tokens.js";
    -import {
    -  hasAudioTranscriptionConfig,
    -  isAudio,
    -  transcribeInboundAudio,
    -} from "./transcription.js";
    -import type { GetReplyOptions, ReplyPayload } from "./types.js";
    -
     export {
       extractElevatedDirective,
       extractReasoningDirective,
       extractThinkDirective,
       extractVerboseDirective,
     } from "./reply/directives.js";
    +export { getReplyFromConfig } from "./reply/get-reply.js";
     export { extractQueueDirective } from "./reply/queue.js";
     export { extractReplyToTag } from "./reply/reply-tags.js";
     export type { GetReplyOptions, ReplyPayload } from "./types.js";
    -
    -const BARE_SESSION_RESET_PROMPT =
    -  "A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning.";
    -
    -function normalizeAllowToken(value?: string) {
    -  if (!value) return "";
    -  return value.trim().toLowerCase();
    -}
    -
    -function slugAllowToken(value?: string) {
    -  if (!value) return "";
    -  let text = value.trim().toLowerCase();
    -  if (!text) return "";
    -  text = text.replace(/^[@#]+/, "");
    -  text = text.replace(/[\s_]+/g, "-");
    -  text = text.replace(/[^a-z0-9-]+/g, "-");
    -  return text.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
    -}
    -
    -const SENDER_PREFIXES = [
    -  ...CHAT_CHANNEL_ORDER,
    -  INTERNAL_MESSAGE_CHANNEL,
    -  "user",
    -  "group",
    -  "channel",
    -];
    -const SENDER_PREFIX_RE = new RegExp(`^(${SENDER_PREFIXES.join("|")}):`, "i");
    -
    -function stripSenderPrefix(value?: string) {
    -  if (!value) return "";
    -  const trimmed = value.trim();
    -  return trimmed.replace(SENDER_PREFIX_RE, "");
    -}
    -
    -const INLINE_SIMPLE_COMMAND_ALIASES = new Map<string, string>([
    -  ["/help", "/help"],
    -  ["/commands", "/commands"],
    -  ["/whoami", "/whoami"],
    -  ["/id", "/whoami"],
    -]);
    -const INLINE_SIMPLE_COMMAND_RE =
    -  /(?:^|\s)\/(help|commands|whoami|id)(?=$|\s|:)/i;
    -
    -const INLINE_STATUS_RE = /(?:^|\s)\/(?:status|usage)(?=$|\s|:)(?:\s*:\s*)?/gi;
    -
    -function extractInlineSimpleCommand(body?: string): {
    -  command: string;
    -  cleaned: string;
    -} | null {
    -  if (!body) return null;
    -  const match = body.match(INLINE_SIMPLE_COMMAND_RE);
    -  if (!match || match.index === undefined) return null;
    -  const alias = `/${match[1].toLowerCase()}`;
    -  const command = INLINE_SIMPLE_COMMAND_ALIASES.get(alias);
    -  if (!command) return null;
    -  const cleaned = body.replace(match[0], " ").replace(/\s+/g, " ").trim();
    -  return { command, cleaned };
    -}
    -
    -function stripInlineStatus(body: string): {
    -  cleaned: string;
    -  didStrip: boolean;
    -} {
    -  const trimmed = body.trim();
    -  if (!trimmed) return { cleaned: "", didStrip: false };
    -  const cleaned = trimmed
    -    .replace(INLINE_STATUS_RE, " ")
    -    .replace(/\s+/g, " ")
    -    .trim();
    -  return { cleaned, didStrip: cleaned !== trimmed };
    -}
    -
    -function resolveElevatedAllowList(
    -  allowFrom: AgentElevatedAllowFromConfig | undefined,
    -  provider: string,
    -  fallbackAllowFrom?: Array<string | number>,
    -): Array<string | number> | undefined {
    -  if (!allowFrom) return fallbackAllowFrom;
    -  const value = allowFrom[provider];
    -  return Array.isArray(value) ? value : fallbackAllowFrom;
    -}
    -
    -function isApprovedElevatedSender(params: {
    -  provider: string;
    -  ctx: MsgContext;
    -  allowFrom?: AgentElevatedAllowFromConfig;
    -  fallbackAllowFrom?: Array<string | number>;
    -}): boolean {
    -  const rawAllow = resolveElevatedAllowList(
    -    params.allowFrom,
    -    params.provider,
    -    params.fallbackAllowFrom,
    -  );
    -  if (!rawAllow || rawAllow.length === 0) return false;
    -
    -  const allowTokens = rawAllow
    -    .map((entry) => String(entry).trim())
    -    .filter(Boolean);
    -  if (allowTokens.length === 0) return false;
    -  if (allowTokens.some((entry) => entry === "*")) return true;
    -
    -  const tokens = new Set<string>();
    -  const addToken = (value?: string) => {
    -    if (!value) return;
    -    const trimmed = value.trim();
    -    if (!trimmed) return;
    -    tokens.add(trimmed);
    -    const normalized = normalizeAllowToken(trimmed);
    -    if (normalized) tokens.add(normalized);
    -    const slugged = slugAllowToken(trimmed);
    -    if (slugged) tokens.add(slugged);
    -  };
    -
    -  addToken(params.ctx.SenderName);
    -  addToken(params.ctx.SenderUsername);
    -  addToken(params.ctx.SenderTag);
    -  addToken(params.ctx.SenderE164);
    -  addToken(params.ctx.From);
    -  addToken(stripSenderPrefix(params.ctx.From));
    -  addToken(params.ctx.To);
    -  addToken(stripSenderPrefix(params.ctx.To));
    -
    -  for (const rawEntry of allowTokens) {
    -    const entry = rawEntry.trim();
    -    if (!entry) continue;
    -    const stripped = stripSenderPrefix(entry);
    -    if (tokens.has(entry) || tokens.has(stripped)) return true;
    -    const normalized = normalizeAllowToken(stripped);
    -    if (normalized && tokens.has(normalized)) return true;
    -    const slugged = slugAllowToken(stripped);
    -    if (slugged && tokens.has(slugged)) return true;
    -  }
    -
    -  return false;
    -}
    -
    -function resolveElevatedPermissions(params: {
    -  cfg: ClawdbotConfig;
    -  agentId: string;
    -  ctx: MsgContext;
    -  provider: string;
    -}): {
    -  enabled: boolean;
    -  allowed: boolean;
    -  failures: Array<{ gate: string; key: string }>;
    -} {
    -  const globalConfig = params.cfg.tools?.elevated;
    -  const agentConfig = resolveAgentConfig(params.cfg, params.agentId)?.tools
    -    ?.elevated;
    -  const globalEnabled = globalConfig?.enabled !== false;
    -  const agentEnabled = agentConfig?.enabled !== false;
    -  const enabled = globalEnabled && agentEnabled;
    -  const failures: Array<{ gate: string; key: string }> = [];
    -  if (!globalEnabled)
    -    failures.push({ gate: "enabled", key: "tools.elevated.enabled" });
    -  if (!agentEnabled)
    -    failures.push({
    -      gate: "enabled",
    -      key: "agents.list[].tools.elevated.enabled",
    -    });
    -  if (!enabled) return { enabled, allowed: false, failures };
    -  if (!params.provider) {
    -    failures.push({ gate: "provider", key: "ctx.Provider" });
    -    return { enabled, allowed: false, failures };
    -  }
    -
    -  const normalizedProvider = normalizeChannelId(params.provider);
    -  const dockFallbackAllowFrom = normalizedProvider
    -    ? getChannelDock(normalizedProvider)?.elevated?.allowFromFallback?.({
    -        cfg: params.cfg,
    -        accountId: params.ctx.AccountId,
    -      })
    -    : undefined;
    -  const fallbackAllowFrom = dockFallbackAllowFrom;
    -  const globalAllowed = isApprovedElevatedSender({
    -    provider: params.provider,
    -    ctx: params.ctx,
    -    allowFrom: globalConfig?.allowFrom,
    -    fallbackAllowFrom,
    -  });
    -  if (!globalAllowed) {
    -    failures.push({
    -      gate: "allowFrom",
    -      key: `tools.elevated.allowFrom.${params.provider}`,
    -    });
    -    return { enabled, allowed: false, failures };
    -  }
    -
    -  const agentAllowed = agentConfig?.allowFrom
    -    ? isApprovedElevatedSender({
    -        provider: params.provider,
    -        ctx: params.ctx,
    -        allowFrom: agentConfig.allowFrom,
    -        fallbackAllowFrom,
    -      })
    -    : true;
    -  if (!agentAllowed) {
    -    failures.push({
    -      gate: "allowFrom",
    -      key: `agents.list[].tools.elevated.allowFrom.${params.provider}`,
    -    });
    -  }
    -  return { enabled, allowed: globalAllowed && agentAllowed, failures };
    -}
    -
    -function formatElevatedUnavailableMessage(params: {
    -  runtimeSandboxed: boolean;
    -  failures: Array<{ gate: string; key: string }>;
    -  sessionKey?: string;
    -}): string {
    -  const lines: string[] = [];
    -  lines.push(
    -    `elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
    -  );
    -  if (params.failures.length > 0) {
    -    lines.push(
    -      `Failing gates: ${params.failures
    -        .map((f) => `${f.gate} (${f.key})`)
    -        .join(", ")}`,
    -    );
    -  } else {
    -    lines.push(
    -      "Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>).",
    -    );
    -  }
    -  lines.push("Fix-it keys:");
    -  lines.push("- tools.elevated.enabled");
    -  lines.push("- tools.elevated.allowFrom.<provider>");
    -  lines.push("- agents.list[].tools.elevated.enabled");
    -  lines.push("- agents.list[].tools.elevated.allowFrom.<provider>");
    -  if (params.sessionKey) {
    -    lines.push(`See: clawdbot sandbox explain --session ${params.sessionKey}`);
    -  }
    -  return lines.join("\n");
    -}
    -
    -export async function getReplyFromConfig(
    -  ctx: MsgContext,
    -  opts?: GetReplyOptions,
    -  configOverride?: ClawdbotConfig,
    -): Promise<ReplyPayload | ReplyPayload[] | undefined> {
    -  const cfg = configOverride ?? loadConfig();
    -  const agentId = resolveSessionAgentId({
    -    sessionKey: ctx.SessionKey,
    -    config: cfg,
    -  });
    -  const agentCfg = cfg.agents?.defaults;
    -  const sessionCfg = cfg.session;
    -  const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
    -    cfg,
    -    agentId,
    -  });
    -  let provider = defaultProvider;
    -  let model = defaultModel;
    -  if (opts?.isHeartbeat) {
    -    const heartbeatRaw = agentCfg?.heartbeat?.model?.trim() ?? "";
    -    const heartbeatRef = heartbeatRaw
    -      ? resolveModelRefFromString({
    -          raw: heartbeatRaw,
    -          defaultProvider,
    -          aliasIndex,
    -        })
    -      : null;
    -    if (heartbeatRef) {
    -      provider = heartbeatRef.ref.provider;
    -      model = heartbeatRef.ref.model;
    -    }
    -  }
    -
    -  const workspaceDirRaw =
    -    resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR;
    -  const workspace = await ensureAgentWorkspace({
    -    dir: workspaceDirRaw,
    -    ensureBootstrapFiles: !agentCfg?.skipBootstrap,
    -  });
    -  const workspaceDir = workspace.dir;
    -  const agentDir = resolveAgentDir(cfg, agentId);
    -  const timeoutMs = resolveAgentTimeoutMs({ cfg });
    -  const configuredTypingSeconds =
    -    agentCfg?.typingIntervalSeconds ?? sessionCfg?.typingIntervalSeconds;
    -  const typingIntervalSeconds =
    -    typeof configuredTypingSeconds === "number" ? configuredTypingSeconds : 6;
    -  const typing = createTypingController({
    -    onReplyStart: opts?.onReplyStart,
    -    typingIntervalSeconds,
    -    silentToken: SILENT_REPLY_TOKEN,
    -    log: defaultRuntime.log,
    -  });
    -  opts?.onTypingController?.(typing);
    -
    -  let transcribedText: string | undefined;
    -  if (hasAudioTranscriptionConfig(cfg) && isAudio(ctx.MediaType)) {
    -    const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime);
    -    if (transcribed?.text) {
    -      transcribedText = transcribed.text;
    -      ctx.Body = transcribed.text;
    -      ctx.Transcript = transcribed.text;
    -      logVerbose("Replaced Body with audio transcript for reply flow");
    -    }
    -  }
    -
    -  const commandAuthorized = ctx.CommandAuthorized ?? true;
    -  resolveCommandAuthorization({
    -    ctx,
    -    cfg,
    -    commandAuthorized,
    -  });
    -  const sessionState = await initSessionState({
    -    ctx,
    -    cfg,
    -    commandAuthorized,
    -  });
    -  let {
    -    sessionCtx,
    -    sessionEntry,
    -    sessionStore,
    -    sessionKey,
    -    sessionId,
    -    isNewSession,
    -    systemSent,
    -    abortedLastRun,
    -    storePath,
    -    sessionScope,
    -    groupResolution,
    -    isGroup,
    -    triggerBodyNormalized,
    -  } = sessionState;
    -
    -  // Prefer CommandBody/RawBody (clean message without structural context) for directive parsing.
    -  // Keep `Body`/`BodyStripped` as the best-available prompt text (may include context).
    -  const commandSource =
    -    sessionCtx.CommandBody ??
    -    sessionCtx.RawBody ??
    -    sessionCtx.BodyStripped ??
    -    sessionCtx.Body ??
    -    "";
    -  const command = buildCommandContext({
    -    ctx,
    -    cfg,
    -    agentId,
    -    sessionKey,
    -    isGroup,
    -    triggerBodyNormalized,
    -    commandAuthorized,
    -  });
    -  const allowTextCommands = shouldHandleTextCommands({
    -    cfg,
    -    surface: command.surface,
    -    commandSource: ctx.CommandSource,
    -  });
    -  const clearInlineDirectives = (cleaned: string): InlineDirectives => ({
    -    cleaned,
    -    hasThinkDirective: false,
    -    thinkLevel: undefined,
    -    rawThinkLevel: undefined,
    -    hasVerboseDirective: false,
    -    verboseLevel: undefined,
    -    rawVerboseLevel: undefined,
    -    hasReasoningDirective: false,
    -    reasoningLevel: undefined,
    -    rawReasoningLevel: undefined,
    -    hasElevatedDirective: false,
    -    elevatedLevel: undefined,
    -    rawElevatedLevel: undefined,
    -    hasStatusDirective: false,
    -    hasModelDirective: false,
    -    rawModelDirective: undefined,
    -    hasQueueDirective: false,
    -    queueMode: undefined,
    -    queueReset: false,
    -    rawQueueMode: undefined,
    -    debounceMs: undefined,
    -    cap: undefined,
    -    dropPolicy: undefined,
    -    rawDebounce: undefined,
    -    rawCap: undefined,
    -    rawDrop: undefined,
    -    hasQueueOptions: false,
    -  });
    -  const reservedCommands = new Set(
    -    listChatCommands().flatMap((cmd) =>
    -      cmd.textAliases.map((a) => a.replace(/^\//, "").toLowerCase()),
    -    ),
    -  );
    -  const configuredAliases = Object.values(cfg.agents?.defaults?.models ?? {})
    -    .map((entry) => entry.alias?.trim())
    -    .filter((alias): alias is string => Boolean(alias))
    -    .filter((alias) => !reservedCommands.has(alias.toLowerCase()));
    -  const allowStatusDirective = allowTextCommands && command.isAuthorizedSender;
    -  let parsedDirectives = parseInlineDirectives(commandSource, {
    -    modelAliases: configuredAliases,
    -    allowStatusDirective,
    -  });
    -  const hasInlineStatus =
    -    parsedDirectives.hasStatusDirective &&
    -    parsedDirectives.cleaned.trim().length > 0;
    -  if (hasInlineStatus) {
    -    parsedDirectives = {
    -      ...parsedDirectives,
    -      hasStatusDirective: false,
    -    };
    -  }
    -  if (
    -    isGroup &&
    -    ctx.WasMentioned !== true &&
    -    parsedDirectives.hasElevatedDirective
    -  ) {
    -    if (parsedDirectives.elevatedLevel !== "off") {
    -      parsedDirectives = {
    -        ...parsedDirectives,
    -        hasElevatedDirective: false,
    -        elevatedLevel: undefined,
    -        rawElevatedLevel: undefined,
    -      };
    -    }
    -  }
    -  const hasInlineDirective =
    -    parsedDirectives.hasThinkDirective ||
    -    parsedDirectives.hasVerboseDirective ||
    -    parsedDirectives.hasReasoningDirective ||
    -    parsedDirectives.hasElevatedDirective ||
    -    parsedDirectives.hasModelDirective ||
    -    parsedDirectives.hasQueueDirective;
    -  if (hasInlineDirective) {
    -    const stripped = stripStructuralPrefixes(parsedDirectives.cleaned);
    -    const noMentions = isGroup
    -      ? stripMentions(stripped, ctx, cfg, agentId)
    -      : stripped;
    -    if (noMentions.trim().length > 0) {
    -      const directiveOnlyCheck = parseInlineDirectives(noMentions, {
    -        modelAliases: configuredAliases,
    -      });
    -      if (directiveOnlyCheck.cleaned.trim().length > 0) {
    -        const allowInlineStatus =
    -          parsedDirectives.hasStatusDirective &&
    -          allowTextCommands &&
    -          command.isAuthorizedSender;
    -        parsedDirectives = allowInlineStatus
    -          ? {
    -              ...clearInlineDirectives(parsedDirectives.cleaned),
    -              hasStatusDirective: true,
    -            }
    -          : clearInlineDirectives(parsedDirectives.cleaned);
    -      }
    -    }
    -  }
    -  let directives = commandAuthorized
    -    ? parsedDirectives
    -    : {
    -        ...parsedDirectives,
    -        hasThinkDirective: false,
    -        hasVerboseDirective: false,
    -        hasReasoningDirective: false,
    -        hasStatusDirective: false,
    -        hasModelDirective: false,
    -        hasQueueDirective: false,
    -        queueReset: false,
    -      };
    -  const existingBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
    -  let cleanedBody = (() => {
    -    if (!existingBody) return parsedDirectives.cleaned;
    -    if (!sessionCtx.CommandBody && !sessionCtx.RawBody) {
    -      return parseInlineDirectives(existingBody, {
    -        modelAliases: configuredAliases,
    -        allowStatusDirective,
    -      }).cleaned;
    -    }
    -
    -    const markerIndex = existingBody.indexOf(CURRENT_MESSAGE_MARKER);
    -    if (markerIndex < 0) {
    -      return parseInlineDirectives(existingBody, {
    -        modelAliases: configuredAliases,
    -        allowStatusDirective,
    -      }).cleaned;
    -    }
    -
    -    const head = existingBody.slice(
    -      0,
    -      markerIndex + CURRENT_MESSAGE_MARKER.length,
    -    );
    -    const tail = existingBody.slice(
    -      markerIndex + CURRENT_MESSAGE_MARKER.length,
    -    );
    -    const cleanedTail = parseInlineDirectives(tail, {
    -      modelAliases: configuredAliases,
    -      allowStatusDirective,
    -    }).cleaned;
    -    return `${head}${cleanedTail}`;
    -  })();
    -
    -  if (allowStatusDirective) {
    -    cleanedBody = stripInlineStatus(cleanedBody).cleaned;
    -  }
    -
    -  sessionCtx.Body = cleanedBody;
    -  sessionCtx.BodyStripped = cleanedBody;
    -
    -  const messageProviderKey =
    -    sessionCtx.Provider?.trim().toLowerCase() ??
    -    ctx.Provider?.trim().toLowerCase() ??
    -    "";
    -  const elevated = resolveElevatedPermissions({
    -    cfg,
    -    agentId,
    -    ctx,
    -    provider: messageProviderKey,
    -  });
    -  const elevatedEnabled = elevated.enabled;
    -  const elevatedAllowed = elevated.allowed;
    -  const elevatedFailures = elevated.failures;
    -  if (
    -    directives.hasElevatedDirective &&
    -    (!elevatedEnabled || !elevatedAllowed)
    -  ) {
    -    typing.cleanup();
    -    const runtimeSandboxed = resolveSandboxRuntimeStatus({
    -      cfg,
    -      sessionKey: ctx.SessionKey,
    -    }).sandboxed;
    -    return {
    -      text: formatElevatedUnavailableMessage({
    -        runtimeSandboxed,
    -        failures: elevatedFailures,
    -        sessionKey: ctx.SessionKey,
    -      }),
    -    };
    -  }
    -
    -  const requireMention = resolveGroupRequireMention({
    -    cfg,
    -    ctx: sessionCtx,
    -    groupResolution,
    -  });
    -  const defaultActivation = defaultGroupActivation(requireMention);
    -  let resolvedThinkLevel =
    -    (directives.thinkLevel as ThinkLevel | undefined) ??
    -    (sessionEntry?.thinkingLevel as ThinkLevel | undefined) ??
    -    (agentCfg?.thinkingDefault as ThinkLevel | undefined);
    -
    -  const resolvedVerboseLevel =
    -    (directives.verboseLevel as VerboseLevel | undefined) ??
    -    (sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
    -    (agentCfg?.verboseDefault as VerboseLevel | undefined);
    -  const resolvedReasoningLevel: ReasoningLevel =
    -    (directives.reasoningLevel as ReasoningLevel | undefined) ??
    -    (sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ??
    -    "off";
    -  const resolvedElevatedLevel = elevatedAllowed
    -    ? ((directives.elevatedLevel as ElevatedLevel | undefined) ??
    -      (sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
    -      (agentCfg?.elevatedDefault as ElevatedLevel | undefined) ??
    -      "on")
    -    : "off";
    -  const resolvedBlockStreaming =
    -    opts?.disableBlockStreaming === true
    -      ? "off"
    -      : opts?.disableBlockStreaming === false
    -        ? "on"
    -        : agentCfg?.blockStreamingDefault === "on"
    -          ? "on"
    -          : "off";
    -  const resolvedBlockStreamingBreak: "text_end" | "message_end" =
    -    agentCfg?.blockStreamingBreak === "message_end"
    -      ? "message_end"
    -      : "text_end";
    -  const blockStreamingEnabled =
    -    resolvedBlockStreaming === "on" && opts?.disableBlockStreaming !== true;
    -  const blockReplyChunking = blockStreamingEnabled
    -    ? resolveBlockStreamingChunking(
    -        cfg,
    -        sessionCtx.Provider,
    -        sessionCtx.AccountId,
    -      )
    -    : undefined;
    -
    -  const modelState = await createModelSelectionState({
    -    cfg,
    -    agentCfg,
    -    sessionEntry,
    -    sessionStore,
    -    sessionKey,
    -    storePath,
    -    defaultProvider,
    -    defaultModel,
    -    provider,
    -    model,
    -    hasModelDirective: directives.hasModelDirective,
    -  });
    -  provider = modelState.provider;
    -  model = modelState.model;
    -
    -  let contextTokens = resolveContextTokens({
    -    agentCfg,
    -    model,
    -  });
    -
    -  const initialModelLabel = `${provider}/${model}`;
    -  const formatModelSwitchEvent = (label: string, alias?: string) =>
    -    alias
    -      ? `Model switched to ${alias} (${label}).`
    -      : `Model switched to ${label}.`;
    -  const isModelListAlias =
    -    directives.hasModelDirective &&
    -    ["status", "list"].includes(
    -      directives.rawModelDirective?.trim().toLowerCase() ?? "",
    -    );
    -  const effectiveModelDirective = isModelListAlias
    -    ? undefined
    -    : directives.rawModelDirective;
    -
    -  const inlineStatusRequested =
    -    hasInlineStatus && allowTextCommands && command.isAuthorizedSender;
    -
    -  // Inline control directives should apply immediately, even when mixed with text.
    -  let directiveAck: ReplyPayload | undefined;
    -
    -  if (!command.isAuthorizedSender) {
    -    directives = {
    -      ...directives,
    -      hasThinkDirective: false,
    -      hasVerboseDirective: false,
    -      hasReasoningDirective: false,
    -      hasElevatedDirective: false,
    -      hasStatusDirective: false,
    -      hasModelDirective: false,
    -      hasQueueDirective: false,
    -      queueReset: false,
    -    };
    -  }
    -
    -  if (
    -    isDirectiveOnly({
    -      directives,
    -      cleanedBody: directives.cleaned,
    -      ctx,
    -      cfg,
    -      agentId,
    -      isGroup,
    -    })
    -  ) {
    -    if (!command.isAuthorizedSender) {
    -      typing.cleanup();
    -      return undefined;
    -    }
    -    const resolvedDefaultThinkLevel =
    -      (sessionEntry?.thinkingLevel as ThinkLevel | undefined) ??
    -      (agentCfg?.thinkingDefault as ThinkLevel | undefined) ??
    -      (await modelState.resolveDefaultThinkingLevel());
    -    const currentThinkLevel = resolvedDefaultThinkLevel;
    -    const currentVerboseLevel =
    -      (sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
    -      (agentCfg?.verboseDefault as VerboseLevel | undefined);
    -    const currentReasoningLevel =
    -      (sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ?? "off";
    -    const currentElevatedLevel =
    -      (sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
    -      (agentCfg?.elevatedDefault as ElevatedLevel | undefined);
    -    const directiveReply = await handleDirectiveOnly({
    -      cfg,
    -      directives,
    -      sessionEntry,
    -      sessionStore,
    -      sessionKey,
    -      storePath,
    -      elevatedEnabled,
    -      elevatedAllowed,
    -      elevatedFailures,
    -      messageProviderKey,
    -      defaultProvider,
    -      defaultModel,
    -      aliasIndex,
    -      allowedModelKeys: modelState.allowedModelKeys,
    -      allowedModelCatalog: modelState.allowedModelCatalog,
    -      resetModelOverride: modelState.resetModelOverride,
    -      provider,
    -      model,
    -      initialModelLabel,
    -      formatModelSwitchEvent,
    -      currentThinkLevel,
    -      currentVerboseLevel,
    -      currentReasoningLevel,
    -      currentElevatedLevel,
    -    });
    -    let statusReply: ReplyPayload | undefined;
    -    if (
    -      directives.hasStatusDirective &&
    -      allowTextCommands &&
    -      command.isAuthorizedSender
    -    ) {
    -      statusReply = await buildStatusReply({
    -        cfg,
    -        command,
    -        sessionEntry,
    -        sessionKey,
    -        sessionScope,
    -        provider,
    -        model,
    -        contextTokens,
    -        resolvedThinkLevel: resolvedDefaultThinkLevel,
    -        resolvedVerboseLevel: (currentVerboseLevel ?? "off") as VerboseLevel,
    -        resolvedReasoningLevel: (currentReasoningLevel ??
    -          "off") as ReasoningLevel,
    -        resolvedElevatedLevel,
    -        resolveDefaultThinkingLevel: async () => resolvedDefaultThinkLevel,
    -        isGroup,
    -        defaultGroupActivation: () => defaultActivation,
    -      });
    -    }
    -    typing.cleanup();
    -    if (statusReply?.text && directiveReply?.text) {
    -      return { text: `${directiveReply.text}\n${statusReply.text}` };
    -    }
    -    return statusReply ?? directiveReply;
    -  }
    -
    -  const hasAnyDirective =
    -    directives.hasThinkDirective ||
    -    directives.hasVerboseDirective ||
    -    directives.hasReasoningDirective ||
    -    directives.hasElevatedDirective ||
    -    directives.hasModelDirective ||
    -    directives.hasQueueDirective ||
    -    directives.hasStatusDirective;
    -
    -  if (hasAnyDirective && command.isAuthorizedSender) {
    -    const fastLane = await applyInlineDirectivesFastLane({
    -      directives,
    -      commandAuthorized: command.isAuthorizedSender,
    -      ctx,
    -      cfg,
    -      agentId,
    -      isGroup,
    -      sessionEntry,
    -      sessionStore,
    -      sessionKey,
    -      storePath,
    -      elevatedEnabled,
    -      elevatedAllowed,
    -      elevatedFailures,
    -      messageProviderKey,
    -      defaultProvider,
    -      defaultModel,
    -      aliasIndex,
    -      allowedModelKeys: modelState.allowedModelKeys,
    -      allowedModelCatalog: modelState.allowedModelCatalog,
    -      resetModelOverride: modelState.resetModelOverride,
    -      provider,
    -      model,
    -      initialModelLabel,
    -      formatModelSwitchEvent,
    -      agentCfg,
    -      modelState: {
    -        resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
    -        allowedModelKeys: modelState.allowedModelKeys,
    -        allowedModelCatalog: modelState.allowedModelCatalog,
    -        resetModelOverride: modelState.resetModelOverride,
    -      },
    -    });
    -    directiveAck = fastLane.directiveAck;
    -    provider = fastLane.provider;
    -    model = fastLane.model;
    -  }
    -
    -  const persisted = await persistInlineDirectives({
    -    directives,
    -    effectiveModelDirective,
    -    cfg,
    -    agentDir,
    -    sessionEntry,
    -    sessionStore,
    -    sessionKey,
    -    storePath,
    -    elevatedEnabled,
    -    elevatedAllowed,
    -    defaultProvider,
    -    defaultModel,
    -    aliasIndex,
    -    allowedModelKeys: modelState.allowedModelKeys,
    -    provider,
    -    model,
    -    initialModelLabel,
    -    formatModelSwitchEvent,
    -    agentCfg,
    -  });
    -  provider = persisted.provider;
    -  model = persisted.model;
    -  contextTokens = persisted.contextTokens;
    -
    -  const perMessageQueueMode =
    -    directives.hasQueueDirective && !directives.queueReset
    -      ? directives.queueMode
    -      : undefined;
    -  const perMessageQueueOptions =
    -    directives.hasQueueDirective && !directives.queueReset
    -      ? {
    -          debounceMs: directives.debounceMs,
    -          cap: directives.cap,
    -          dropPolicy: directives.dropPolicy,
    -        }
    -      : undefined;
    -
    -  const sendInlineReply = async (reply?: ReplyPayload) => {
    -    if (!reply) return;
    -    if (!opts?.onBlockReply) return;
    -    await opts.onBlockReply(reply);
    -  };
    -
    -  const inlineCommand =
    -    allowTextCommands && command.isAuthorizedSender
    -      ? extractInlineSimpleCommand(cleanedBody)
    -      : null;
    -  if (inlineCommand) {
    -    cleanedBody = inlineCommand.cleaned;
    -    sessionCtx.Body = cleanedBody;
    -    sessionCtx.BodyStripped = cleanedBody;
    -  }
    -
    -  const handleInlineStatus =
    -    !isDirectiveOnly({
    -      directives,
    -      cleanedBody: directives.cleaned,
    -      ctx,
    -      cfg,
    -      agentId,
    -      isGroup,
    -    }) && inlineStatusRequested;
    -  if (handleInlineStatus) {
    -    const inlineStatusReply = await buildStatusReply({
    -      cfg,
    -      command,
    -      sessionEntry,
    -      sessionKey,
    -      sessionScope,
    -      provider,
    -      model,
    -      contextTokens,
    -      resolvedThinkLevel,
    -      resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
    -      resolvedReasoningLevel,
    -      resolvedElevatedLevel,
    -      resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
    -      isGroup,
    -      defaultGroupActivation: () => defaultActivation,
    -    });
    -    await sendInlineReply(inlineStatusReply);
    -    directives = { ...directives, hasStatusDirective: false };
    -  }
    -
    -  if (inlineCommand) {
    -    const inlineCommandContext = {
    -      ...command,
    -      rawBodyNormalized: inlineCommand.command,
    -      commandBodyNormalized: inlineCommand.command,
    -    };
    -    const inlineResult = await handleCommands({
    -      ctx,
    -      cfg,
    -      command: inlineCommandContext,
    -      agentId,
    -      directives,
    -      elevated: {
    -        enabled: elevatedEnabled,
    -        allowed: elevatedAllowed,
    -        failures: elevatedFailures,
    -      },
    -      sessionEntry,
    -      sessionStore,
    -      sessionKey,
    -      storePath,
    -      sessionScope,
    -      workspaceDir,
    -      defaultGroupActivation: () => defaultActivation,
    -      resolvedThinkLevel,
    -      resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
    -      resolvedReasoningLevel,
    -      resolvedElevatedLevel,
    -      resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
    -      provider,
    -      model,
    -      contextTokens,
    -      isGroup,
    -    });
    -    if (inlineResult.reply) {
    -      if (!inlineCommand.cleaned) {
    -        typing.cleanup();
    -        return inlineResult.reply;
    -      }
    -      await sendInlineReply(inlineResult.reply);
    -    }
    -  }
    -
    -  if (directiveAck) {
    -    await sendInlineReply(directiveAck);
    -  }
    -
    -  const isEmptyConfig = Object.keys(cfg).length === 0;
    -  const skipWhenConfigEmpty = command.channelId
    -    ? Boolean(getChannelDock(command.channelId)?.commands?.skipWhenConfigEmpty)
    -    : false;
    -  if (
    -    skipWhenConfigEmpty &&
    -    isEmptyConfig &&
    -    command.from &&
    -    command.to &&
    -    command.from !== command.to
    -  ) {
    -    typing.cleanup();
    -    return undefined;
    -  }
    -
    -  if (!sessionEntry && command.abortKey) {
    -    abortedLastRun = getAbortMemory(command.abortKey) ?? false;
    -  }
    -
    -  const commandResult = await handleCommands({
    -    ctx,
    -    cfg,
    -    command,
    -    agentId,
    -    directives,
    -    elevated: {
    -      enabled: elevatedEnabled,
    -      allowed: elevatedAllowed,
    -      failures: elevatedFailures,
    -    },
    -    sessionEntry,
    -    sessionStore,
    -    sessionKey,
    -    storePath,
    -    sessionScope,
    -    workspaceDir,
    -    defaultGroupActivation: () => defaultActivation,
    -    resolvedThinkLevel,
    -    resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
    -    resolvedReasoningLevel,
    -    resolvedElevatedLevel,
    -    resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
    -    provider,
    -    model,
    -    contextTokens,
    -    isGroup,
    -  });
    -  if (!commandResult.shouldContinue) {
    -    typing.cleanup();
    -    return commandResult.reply;
    -  }
    -
    -  await stageSandboxMedia({
    -    ctx,
    -    sessionCtx,
    -    cfg,
    -    sessionKey,
    -    workspaceDir,
    -  });
    -
    -  const isFirstTurnInSession = isNewSession || !systemSent;
    -  const isGroupChat = sessionCtx.ChatType === "group";
    -  const wasMentioned = ctx.WasMentioned === true;
    -  const isHeartbeat = opts?.isHeartbeat === true;
    -  const typingMode = resolveTypingMode({
    -    configured: sessionCfg?.typingMode ?? agentCfg?.typingMode,
    -    isGroupChat,
    -    wasMentioned,
    -    isHeartbeat,
    -  });
    -  const typingSignals = createTypingSignaler({
    -    typing,
    -    mode: typingMode,
    -    isHeartbeat,
    -  });
    -  const shouldInjectGroupIntro = Boolean(
    -    isGroupChat &&
    -      (isFirstTurnInSession || sessionEntry?.groupActivationNeedsSystemIntro),
    -  );
    -  const groupIntro = shouldInjectGroupIntro
    -    ? buildGroupIntro({
    -        cfg,
    -        sessionCtx,
    -        sessionEntry,
    -        defaultActivation,
    -        silentToken: SILENT_REPLY_TOKEN,
    -      })
    -    : "";
    -  const groupSystemPrompt = sessionCtx.GroupSystemPrompt?.trim() ?? "";
    -  const extraSystemPrompt = [groupIntro, groupSystemPrompt]
    -    .filter(Boolean)
    -    .join("\n\n");
    -  const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
    -  // Use CommandBody/RawBody for bare reset detection (clean message without structural context).
    -  const rawBodyTrimmed = (
    -    ctx.CommandBody ??
    -    ctx.RawBody ??
    -    ctx.Body ??
    -    ""
    -  ).trim();
    -  const baseBodyTrimmedRaw = baseBody.trim();
    -  if (
    -    allowTextCommands &&
    -    (!commandAuthorized || !command.isAuthorizedSender) &&
    -    !baseBodyTrimmedRaw &&
    -    hasControlCommand(commandSource, cfg)
    -  ) {
    -    typing.cleanup();
    -    return undefined;
    -  }
    -  const isBareSessionReset =
    -    isNewSession &&
    -    baseBodyTrimmedRaw.length === 0 &&
    -    rawBodyTrimmed.length > 0;
    -  const baseBodyFinal = isBareSessionReset
    -    ? BARE_SESSION_RESET_PROMPT
    -    : baseBody;
    -  const baseBodyTrimmed = baseBodyFinal.trim();
    -  if (!baseBodyTrimmed) {
    -    await typing.onReplyStart();
    -    logVerbose("Inbound body empty after normalization; skipping agent run");
    -    typing.cleanup();
    -    return {
    -      text: "I didn't receive any text in your message. Please resend or add a caption.",
    -    };
    -  }
    -  let prefixedBodyBase = await applySessionHints({
    -    baseBody: baseBodyFinal,
    -    abortedLastRun,
    -    sessionEntry,
    -    sessionStore,
    -    sessionKey,
    -    storePath,
    -    abortKey: command.abortKey,
    -    messageId: sessionCtx.MessageSid,
    -  });
    -  const isGroupSession =
    -    sessionEntry?.chatType === "group" || sessionEntry?.chatType === "room";
    -  const isMainSession =
    -    !isGroupSession && sessionKey === normalizeMainKey(sessionCfg?.mainKey);
    -  prefixedBodyBase = await prependSystemEvents({
    -    cfg,
    -    sessionKey,
    -    isMainSession,
    -    isNewSession,
    -    prefixedBodyBase,
    -  });
    -  const threadStarterBody = ctx.ThreadStarterBody?.trim();
    -  const threadStarterNote =
    -    isNewSession && threadStarterBody
    -      ? `[Thread starter - for context]\n${threadStarterBody}`
    -      : undefined;
    -  const skillResult = await ensureSkillSnapshot({
    -    sessionEntry,
    -    sessionStore,
    -    sessionKey,
    -    storePath,
    -    sessionId,
    -    isFirstTurnInSession,
    -    workspaceDir,
    -    cfg,
    -    skillFilter: opts?.skillFilter,
    -  });
    -  sessionEntry = skillResult.sessionEntry ?? sessionEntry;
    -  systemSent = skillResult.systemSent;
    -  const skillsSnapshot = skillResult.skillsSnapshot;
    -  const prefixedBody = transcribedText
    -    ? [threadStarterNote, prefixedBodyBase, `Transcript:\n${transcribedText}`]
    -        .filter(Boolean)
    -        .join("\n\n")
    -    : [threadStarterNote, prefixedBodyBase].filter(Boolean).join("\n\n");
    -  const mediaNote = buildInboundMediaNote(ctx);
    -  const mediaReplyHint = mediaNote
    -    ? "To send an image back, add a line like: MEDIA:https://example.com/image.jpg (no spaces). Keep caption in the text body."
    -    : undefined;
    -  let prefixedCommandBody = mediaNote
    -    ? [mediaNote, mediaReplyHint, prefixedBody ?? ""]
    -        .filter(Boolean)
    -        .join("\n")
    -        .trim()
    -    : prefixedBody;
    -  if (!resolvedThinkLevel && prefixedCommandBody) {
    -    const parts = prefixedCommandBody.split(/\s+/);
    -    const maybeLevel = normalizeThinkLevel(parts[0]);
    -    if (
    -      maybeLevel &&
    -      (maybeLevel !== "xhigh" || supportsXHighThinking(provider, model))
    -    ) {
    -      resolvedThinkLevel = maybeLevel;
    -      prefixedCommandBody = parts.slice(1).join(" ").trim();
    -    }
    -  }
    -  if (!resolvedThinkLevel) {
    -    resolvedThinkLevel = await modelState.resolveDefaultThinkingLevel();
    -  }
    -  if (
    -    resolvedThinkLevel === "xhigh" &&
    -    !supportsXHighThinking(provider, model)
    -  ) {
    -    const explicitThink =
    -      directives.hasThinkDirective && directives.thinkLevel !== undefined;
    -    if (explicitThink) {
    -      typing.cleanup();
    -      return {
    -        text: `Thinking level "xhigh" is only supported for ${formatXHighModelHint()}. Use /think high or switch to one of those models.`,
    -      };
    -    }
    -    resolvedThinkLevel = "high";
    -    if (
    -      sessionEntry &&
    -      sessionStore &&
    -      sessionKey &&
    -      sessionEntry.thinkingLevel === "xhigh"
    -    ) {
    -      sessionEntry.thinkingLevel = "high";
    -      sessionEntry.updatedAt = Date.now();
    -      sessionStore[sessionKey] = sessionEntry;
    -      if (storePath) {
    -        await saveSessionStore(storePath, sessionStore);
    -      }
    -    }
    -  }
    -  const sessionIdFinal = sessionId ?? crypto.randomUUID();
    -  const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
    -  const queueBodyBase = transcribedText
    -    ? [threadStarterNote, baseBodyFinal, `Transcript:\n${transcribedText}`]
    -        .filter(Boolean)
    -        .join("\n\n")
    -    : [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n");
    -  const queuedBody = mediaNote
    -    ? [mediaNote, mediaReplyHint, queueBodyBase]
    -        .filter(Boolean)
    -        .join("\n")
    -        .trim()
    -    : queueBodyBase;
    -  const resolvedQueue = resolveQueueSettings({
    -    cfg,
    -    channel: sessionCtx.Provider,
    -    sessionEntry,
    -    inlineMode: perMessageQueueMode,
    -    inlineOptions: perMessageQueueOptions,
    -  });
    -  const sessionLaneKey = resolveEmbeddedSessionLane(
    -    sessionKey ?? sessionIdFinal,
    -  );
    -  const laneSize = getQueueSize(sessionLaneKey);
    -  if (resolvedQueue.mode === "interrupt" && laneSize > 0) {
    -    const cleared = clearCommandLane(sessionLaneKey);
    -    const aborted = abortEmbeddedPiRun(sessionIdFinal);
    -    logVerbose(
    -      `Interrupting ${sessionLaneKey} (cleared ${cleared}, aborted=${aborted})`,
    -    );
    -  }
    -  const queueKey = sessionKey ?? sessionIdFinal;
    -  const isActive = isEmbeddedPiRunActive(sessionIdFinal);
    -  const isStreaming = isEmbeddedPiRunStreaming(sessionIdFinal);
    -  const shouldSteer =
    -    resolvedQueue.mode === "steer" || resolvedQueue.mode === "steer-backlog";
    -  const shouldFollowup =
    -    resolvedQueue.mode === "followup" ||
    -    resolvedQueue.mode === "collect" ||
    -    resolvedQueue.mode === "steer-backlog";
    -  const authProfileId = sessionEntry?.authProfileOverride;
    -  const followupRun = {
    -    prompt: queuedBody,
    -    messageId: sessionCtx.MessageSid,
    -    summaryLine: baseBodyTrimmedRaw,
    -    enqueuedAt: Date.now(),
    -    // Originating channel for reply routing.
    -    originatingChannel: ctx.OriginatingChannel,
    -    originatingTo: ctx.OriginatingTo,
    -    originatingAccountId: ctx.AccountId,
    -    originatingThreadId: ctx.MessageThreadId,
    -    run: {
    -      agentId,
    -      agentDir,
    -      sessionId: sessionIdFinal,
    -      sessionKey,
    -      messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
    -      agentAccountId: sessionCtx.AccountId,
    -      sessionFile,
    -      workspaceDir,
    -      config: cfg,
    -      skillsSnapshot,
    -      provider,
    -      model,
    -      authProfileId,
    -      thinkLevel: resolvedThinkLevel,
    -      verboseLevel: resolvedVerboseLevel,
    -      reasoningLevel: resolvedReasoningLevel,
    -      elevatedLevel: resolvedElevatedLevel,
    -      bashElevated: {
    -        enabled: elevatedEnabled,
    -        allowed: elevatedAllowed,
    -        defaultLevel: resolvedElevatedLevel ?? "off",
    -      },
    -      timeoutMs,
    -      blockReplyBreak: resolvedBlockStreamingBreak,
    -      ownerNumbers:
    -        command.ownerList.length > 0 ? command.ownerList : undefined,
    -      extraSystemPrompt: extraSystemPrompt || undefined,
    -      ...(isReasoningTagProvider(provider) ? { enforceFinalTag: true } : {}),
    -    },
    -  };
    -
    -  if (typingSignals.shouldStartImmediately) {
    -    await typingSignals.signalRunStart();
    -  }
    -
    -  return runReplyAgent({
    -    commandBody: prefixedCommandBody,
    -    followupRun,
    -    queueKey,
    -    resolvedQueue,
    -    shouldSteer,
    -    shouldFollowup,
    -    isActive,
    -    isStreaming,
    -    opts,
    -    typing,
    -    sessionEntry,
    -    sessionStore,
    -    sessionKey,
    -    storePath,
    -    defaultModel,
    -    agentCfgContextTokens: agentCfg?.contextTokens,
    -    resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
    -    isNewSession,
    -    blockStreamingEnabled,
    -    blockReplyChunking,
    -    resolvedBlockStreamingBreak,
    -    sessionCtx,
    -    shouldInjectGroupIntro,
    -    typingMode,
    -  });
    -}
    -
    -async function stageSandboxMedia(params: {
    -  ctx: MsgContext;
    -  sessionCtx: TemplateContext;
    -  cfg: ClawdbotConfig;
    -  sessionKey?: string;
    -  workspaceDir: string;
    -}) {
    -  const { ctx, sessionCtx, cfg, sessionKey, workspaceDir } = params;
    -  const hasPathsArray =
    -    Array.isArray(ctx.MediaPaths) && ctx.MediaPaths.length > 0;
    -  const pathsFromArray = Array.isArray(ctx.MediaPaths)
    -    ? ctx.MediaPaths
    -    : undefined;
    -  const rawPaths =
    -    pathsFromArray && pathsFromArray.length > 0
    -      ? pathsFromArray
    -      : ctx.MediaPath?.trim()
    -        ? [ctx.MediaPath.trim()]
    -        : [];
    -  if (rawPaths.length === 0 || !sessionKey) return;
    -
    -  const sandbox = await ensureSandboxWorkspaceForSession({
    -    config: cfg,
    -    sessionKey,
    -    workspaceDir,
    -  });
    -  if (!sandbox) return;
    -
    -  const resolveAbsolutePath = (value: string): string | null => {
    -    let resolved = value.trim();
    -    if (!resolved) return null;
    -    if (resolved.startsWith("file://")) {
    -      try {
    -        resolved = fileURLToPath(resolved);
    -      } catch {
    -        return null;
    -      }
    -    }
    -    if (!path.isAbsolute(resolved)) return null;
    -    return resolved;
    -  };
    -
    -  try {
    -    const destDir = path.join(sandbox.workspaceDir, "media", "inbound");
    -    await fs.mkdir(destDir, { recursive: true });
    -
    -    const usedNames = new Set<string>();
    -    const staged = new Map<string, string>(); // absolute source -> relative sandbox path
    -
    -    for (const raw of rawPaths) {
    -      const source = resolveAbsolutePath(raw);
    -      if (!source) continue;
    -      if (staged.has(source)) continue;
    -
    -      const baseName = path.basename(source);
    -      if (!baseName) continue;
    -      const parsed = path.parse(baseName);
    -      let fileName = baseName;
    -      let suffix = 1;
    -      while (usedNames.has(fileName)) {
    -        fileName = `${parsed.name}-${suffix}${parsed.ext}`;
    -        suffix += 1;
    -      }
    -      usedNames.add(fileName);
    -
    -      const dest = path.join(destDir, fileName);
    -      await fs.copyFile(source, dest);
    -      const relative = path.posix.join("media", "inbound", fileName);
    -      staged.set(source, relative);
    -    }
    -
    -    const rewriteIfStaged = (value: string | undefined): string | undefined => {
    -      const raw = value?.trim();
    -      if (!raw) return value;
    -      const abs = resolveAbsolutePath(raw);
    -      if (!abs) return value;
    -      const mapped = staged.get(abs);
    -      return mapped ?? value;
    -    };
    -
    -    const nextMediaPaths = hasPathsArray
    -      ? rawPaths.map((p) => rewriteIfStaged(p) ?? p)
    -      : undefined;
    -    if (nextMediaPaths) {
    -      ctx.MediaPaths = nextMediaPaths;
    -      sessionCtx.MediaPaths = nextMediaPaths;
    -      ctx.MediaPath = nextMediaPaths[0];
    -      sessionCtx.MediaPath = nextMediaPaths[0];
    -    } else {
    -      const rewritten = rewriteIfStaged(ctx.MediaPath);
    -      if (rewritten && rewritten !== ctx.MediaPath) {
    -        ctx.MediaPath = rewritten;
    -        sessionCtx.MediaPath = rewritten;
    -      }
    -    }
    -
    -    if (Array.isArray(ctx.MediaUrls) && ctx.MediaUrls.length > 0) {
    -      const nextUrls = ctx.MediaUrls.map((u) => rewriteIfStaged(u) ?? u);
    -      ctx.MediaUrls = nextUrls;
    -      sessionCtx.MediaUrls = nextUrls;
    -    }
    -    const rewrittenUrl = rewriteIfStaged(ctx.MediaUrl);
    -    if (rewrittenUrl && rewrittenUrl !== ctx.MediaUrl) {
    -      ctx.MediaUrl = rewrittenUrl;
    -      sessionCtx.MediaUrl = rewrittenUrl;
    -    }
    -  } catch (err) {
    -    logVerbose(`Failed to stage inbound media for sandbox: ${String(err)}`);
    -  }
    -}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.