@integrity-labs/agt-cli 0.27.81 → 0.27.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agt.js +3 -3
- package/dist/{chunk-FD5YRWYC.js → chunk-3JXDBRNG.js} +6 -2
- package/dist/{chunk-FD5YRWYC.js.map → chunk-3JXDBRNG.js.map} +1 -1
- package/dist/{chunk-CGUXKOUF.js → chunk-AN5X6CN2.js} +72 -3
- package/dist/chunk-AN5X6CN2.js.map +1 -0
- package/dist/{claude-pair-runtime-BK76FFIY.js → claude-pair-runtime-RTM4GWZG.js} +2 -2
- package/dist/lib/manager-worker.js +15 -6
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/{persistent-session-LGKYKSBP.js → persistent-session-B5SRS4N4.js} +2 -2
- package/dist/{responsiveness-probe-C6ZWB26H.js → responsiveness-probe-2QWNZTF4.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-CGUXKOUF.js.map +0 -1
- /package/dist/{claude-pair-runtime-BK76FFIY.js.map → claude-pair-runtime-RTM4GWZG.js.map} +0 -0
- /package/dist/{persistent-session-LGKYKSBP.js.map → persistent-session-B5SRS4N4.js.map} +0 -0
- /package/dist/{responsiveness-probe-C6ZWB26H.js.map → responsiveness-probe-2QWNZTF4.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/persistent-session.ts","../src/lib/mcp-sanitize.ts","../src/lib/claude-tools.ts","../src/lib/mcp-env-probe.ts","../src/lib/daily-session.ts","../../../packages/core/src/runtime/session-probe.ts","../src/lib/claude-dialogs.ts","../src/lib/channel-input-watchdog.ts"],"sourcesContent":["/**\n * Persistent session manager for Claude Code agents.\n *\n * Hybrid approach:\n * - **tmux** for the interactive session (channels like Slack/Telegram\n * require a real TTY that only tmux provides)\n * - **acpx** for task injection (reliable prompt delivery via --no-wait,\n * avoids the tmux send-keys paste-not-submitting issue)\n *\n * On manager restart, detects existing tmux sessions and reattaches\n * without creating duplicates.\n */\n\nimport { spawn, execSync, execFileSync, type ChildProcess } from 'node:child_process';\nimport { join, dirname } from 'node:path';\nimport { homedir, platform, userInfo } from 'node:os';\nimport { existsSync, readFileSync, readdirSync, writeFileSync, appendFileSync, mkdirSync, chmodSync, copyFileSync, rmSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { sanitizeMcpJson } from './mcp-sanitize.js';\nimport { buildAllowedTools } from './claude-tools.js';\nimport { probeMcpEnvSubstitution, formatMissingVar } from './mcp-env-probe.js';\nimport { claudeModelAlias, isClaudeFastMode } from './claude-model-alias.js';\nimport { reapOrphanChannelMcps } from './orphan-channel-mcp-reaper.js';\nimport { randomUUID } from 'node:crypto';\nimport { markDailySessionSpawn, rotateDailySession } from './daily-session.js';\n// ENG-5832: the tmux/pgrep zombie-probe primitives now live in\n// @augmented/core so the channel servers (packages/mcp) can share the exact\n// same pgrep matching when deciding whether an inbound can be answered. The\n// stateful bookkeeping + dead-session teardown below stays CLI-only.\nimport { probeClaudeProcessInTmux } from '@augmented/core/runtime/session-probe.js';\n// ENG-6017: shared dialog detection/dismissal (also consumed by the\n// channel-input-watchdog) plus input-box extraction for the inject-time\n// pane hygiene below.\nimport {\n isLoginPickerVisible,\n isResumeModeDialogVisible,\n sweepDialogs,\n sendDialogKeys,\n simpleTextHash,\n} from './claude-dialogs.js';\nimport { extractInputBoxText } from './channel-input-watchdog.js';\n\n/**\n * When running as root on Linux, the tmux-spawned claude process reads\n * ~/.claude/.credentials.json from /root. But operators log in via `claude\n * /login` as ssm-user or ec2-user, leaving creds under their own home.\n * Copy the first valid creds file into /root/.claude so claude (running as\n * root inside tmux) finds them. Idempotent — safe to call on every spawn.\n *\n * Returns true if a copy was made (or the file is already up to date),\n * false if no creds could be found at all.\n */\nfunction syncClaudeCredsToRoot(): boolean {\n if (platform() !== 'linux') return true;\n if (typeof process.getuid !== 'function' || process.getuid() !== 0) return true;\n\n // Fast path: pair-via-browser writes creds directly to /root/.claude\n // (the throwaway claude session runs as root). If they're already\n // there, no sync needed.\n for (const filename of ['.credentials.json', 'credentials.json']) {\n if (existsSync(join('/root/.claude', filename))) return true;\n }\n\n // Legacy path: an operator ran `claude /login` interactively as\n // ec2-user. Find any /home/*/.claude credentials and copy them up.\n let sourcePath: string | null = null;\n try {\n const entries = readdirSync('/home', { withFileTypes: true });\n outer: for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n // Both filenames Claude Code has historically used — keep in sync\n // with findClaudeCredentialsPaths() in claude-auth-detect.ts.\n for (const filename of ['.credentials.json', 'credentials.json']) {\n const candidate = join('/home', entry.name, '.claude', filename);\n if (existsSync(candidate)) {\n sourcePath = candidate;\n break outer;\n }\n }\n }\n } catch { /* no /home or unreadable — fall through */ }\n\n if (!sourcePath) return false;\n\n const targetDir = '/root/.claude';\n // Preserve source filename so the resulting file matches what claude's\n // reader expects (it accepts either '.credentials.json' or 'credentials.json').\n const sourceFilename = sourcePath.endsWith('credentials.json') && !sourcePath.endsWith('.credentials.json')\n ? 'credentials.json'\n : '.credentials.json';\n const targetPath = join(targetDir, sourceFilename);\n try {\n if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true, mode: 0o700 });\n copyFileSync(sourcePath, targetPath);\n chmodSync(targetPath, 0o600);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the claude binary to an absolute path. The manager runs under a\n * minimal PATH (cloud-init root env) that doesn't include\n * /home/linuxbrew/.linuxbrew/bin, so a bare `claude` reference in the tmux\n * shell fails immediately — session exits, manager sees it as \"unhealthy\",\n * restarts, loops forever.\n *\n * Cached at first call: claude's location doesn't change between cycles,\n * and `which` spawns aren't free.\n */\nlet cachedClaudePath: string | null = null;\nexport function resolveClaudeBinary(): string {\n if (cachedClaudePath) return cachedClaudePath;\n // Operator override: honour CLAUDE_PATH for non-standard installs.\n const override = process.env.CLAUDE_PATH;\n if (override && existsSync(override)) {\n cachedClaudePath = override;\n return override;\n }\n // Try PATH first — respects an operator's custom install.\n try {\n const out = execSync('which claude 2>/dev/null', { encoding: 'utf-8' }).trim();\n if (out && existsSync(out)) {\n cachedClaudePath = out;\n return out;\n }\n } catch { /* fall through to canonical paths */ }\n const candidates = [\n '/home/linuxbrew/.linuxbrew/bin/claude',\n '/opt/homebrew/bin/claude',\n '/usr/local/bin/claude',\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n cachedClaudePath = p;\n return p;\n }\n }\n // Last resort — let the shell fail so logs show the missing binary.\n return 'claude';\n}\n\n/**\n * ENG-4717: write the wrapper script that the persistent tmux session\n * exec's instead of putting `KEY=VALUE claude ...` directly on the\n * tmux command line. The wrapper sources `.env.integrations` (mode\n * 0600) inside the spawned shell, so secrets land in the exec'd\n * claude process's env without ever crossing the argv boundary that\n * `ps -eo command` reads from.\n *\n * Returns the wrapper path. Always overwrites — kept idempotent so\n * a manager respawn after a credential rotation picks up the new\n * .env.integrations contents on the next session start.\n *\n * Exported for unit tests; production callers go through startSession.\n */\nexport function writePersistentClaudeWrapper(args: {\n projectDir: string;\n claudeBin: string;\n initPrompt: string;\n claudeArgsJoined: string;\n}): string {\n const { projectDir, claudeBin, initPrompt, claudeArgsJoined } = args;\n const envIntegrationsPath = join(projectDir, '.env.integrations');\n const wrapperPath = join(projectDir, '.claude', 'persistent-claude.sh');\n const wrapperLines = [\n '#!/usr/bin/env bash',\n 'set -e',\n // IS_SANDBOX=1 lets claude run under root/sudo with\n // --dangerously-skip-permissions on dedicated EC2 hosts.\n 'export IS_SANDBOX=1',\n ];\n if (existsSync(envIntegrationsPath)) {\n // `set -a` exports every variable assigned by `source`; `set +a`\n // restores the prior state. Anything in .env.integrations becomes\n // an environment variable for the exec'd claude process.\n wrapperLines.push(\n 'set -a',\n `source ${JSON.stringify(envIntegrationsPath)}`,\n 'set +a',\n );\n }\n // ENG-5353: when initPrompt is empty (resumed session), omit the empty\n // positional entirely. Passing `\"\"` as a positional to claude would still\n // append a blank user turn to the resumed transcript and trip the same\n // upstream regression we're trying to avoid.\n const initPromptArg = initPrompt ? `${JSON.stringify(initPrompt)} ` : '';\n wrapperLines.push(\n `exec ${JSON.stringify(claudeBin)} ${initPromptArg}${claudeArgsJoined}`,\n );\n mkdirSync(join(projectDir, '.claude'), { recursive: true });\n // 0700: only the agent process owner can read/execute. The wrapper\n // doesn't contain secrets itself (it sources them from the 0600\n // file) but a hostile reader could still see which env vars get\n // loaded — enough leakage to lock down. The mode option on\n // writeFileSync is only honoured when the file is *created*, so we\n // chmodSync afterwards to enforce 0700 on overwrites too (the\n // common case after the first respawn).\n writeFileSync(wrapperPath, wrapperLines.join('\\n') + '\\n', { mode: 0o700 });\n chmodSync(wrapperPath, 0o700);\n return wrapperPath;\n}\n\n/**\n * Collect MCP server names from the project .mcp.json to build the\n * --allowedTools pattern for tool isolation.\n */\nfunction collectMcpServerNames(mcpConfigPath: string): string[] {\n if (!existsSync(mcpConfigPath)) return [];\n try {\n const data = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));\n const servers = data.mcpServers as Record<string, unknown> | undefined;\n return servers ? Object.keys(servers) : [];\n } catch {\n return [];\n }\n}\n\n// ---------------------------------------------------------------------------\n// acpx binary resolver (used for task injection only)\n// ---------------------------------------------------------------------------\n\nlet _acpxBin: string | null = null;\nfunction getAcpxBin(): string {\n if (_acpxBin) return _acpxBin;\n\n // Walk up from this file to find node_modules/.bin/acpx.\n // Covers: dev (src/lib → ../../node_modules), built (dist/lib → ../../node_modules),\n // and npm global install (lib/node_modules/@scope/pkg/dist/lib → ../../node_modules).\n const moduleDir = dirname(fileURLToPath(import.meta.url));\n let dir = moduleDir;\n for (let i = 0; i < 6; i++) {\n const candidate = join(dir, 'node_modules', '.bin', 'acpx');\n if (existsSync(candidate)) {\n _acpxBin = candidate;\n return _acpxBin;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n try {\n execSync('which acpx', { stdio: 'ignore' });\n _acpxBin = 'acpx';\n return _acpxBin;\n } catch {\n // acpx not available — injection will fall back to tmux send-keys\n return '';\n }\n}\n\n// ---------------------------------------------------------------------------\n// acpx session probe (ENG-5758)\n//\n// `acpx claude exec` requires an existing acpx session for the project. On\n// interactive-tmux agents (channels need a real TTY, so the session is tmux,\n// not acpx-spawned) no acpx session is ever created, so exec exits non-zero\n// (\"No acpx session found\", exit 4). Previously the inject path spawned acpx\n// fire-and-forget with stdio:'ignore' and reported delivered:true regardless —\n// so the failure was invisible and the send-keys fallback was never reached,\n// silently dropping every injected message (kanban /loop, scheduled tasks,\n// synthetic probes). Probe with `list-sessions` and gate the acpx path on it.\n// ---------------------------------------------------------------------------\n\ntype AcpxSessionProbe = (acpxBin: string, projectDir: string) => boolean;\n\nfunction defaultAcpxSessionProbe(acpxBin: string, projectDir: string): boolean {\n try {\n execFileSync(acpxBin, ['claude', 'list-sessions'], {\n cwd: projectDir,\n timeout: 5_000,\n stdio: 'ignore',\n });\n return true; // exit 0 — a session exists for this project\n } catch {\n return false; // non-zero (e.g. exit 4 \"No acpx session found\")\n }\n}\n\nlet acpxSessionProbe: AcpxSessionProbe = defaultAcpxSessionProbe;\nconst acpxSessionAvailableCache = new Map<string, boolean>();\n\nfunction hasAcpxSession(acpxBin: string, projectDir: string, codeName: string): boolean {\n const cached = acpxSessionAvailableCache.get(codeName);\n if (cached !== undefined) return cached;\n const available = acpxSessionProbe(acpxBin, projectDir);\n acpxSessionAvailableCache.set(codeName, available);\n return available;\n}\n\n// ---------------------------------------------------------------------------\n// Types and state\n// ---------------------------------------------------------------------------\n\nexport interface PersistentSessionConfig {\n codeName: string;\n agentId: string;\n projectDir: string;\n mcpConfigPath: string;\n claudeMdPath: string;\n channels: string[];\n devChannels: string[];\n apiHost?: string;\n /**\n * Operator-configured Claude Code auth mode. 'subscription' (default) runs\n * `syncClaudeCredsToRoot()` so claude finds OAuth creds under /root/.claude.\n * 'api_key' puts ANTHROPIC_API_KEY into the spawn env AND deletes any\n * stored OAuth creds so the two auth paths are mutually exclusive.\n */\n claudeAuthMode?: 'subscription' | 'api_key';\n /** Decrypted Anthropic API key. Only used when claudeAuthMode === 'api_key'. */\n anthropicApiKey?: string | null;\n /**\n * ENG-5631: the agent's resolved primary model as a full platform model\n * name (e.g. `claude-sonnet-4-6`, possibly with an `openrouter/anthropic/`\n * prefix). The launcher reduces it to a family alias via `claudeModelAlias`\n * and passes `--model <alias>` to the `claude` spawn — without this, a\n * subscription agent ignores its platform model setting and runs the auth\n * tier's default (Opus 4.7 on Max). When empty/unknown, no `--model` flag is\n * passed and Claude Code uses the tier default.\n */\n primaryModel?: string | null;\n /**\n * ENG-5051: per-session run UUID. When set, exported into the tmux\n * session env so Claude Code's ${AGT_RUN_ID} placeholder substitution\n * in .mcp.json resolves to a real run, unblocking the cloud-broker\n * MCP which 400s when AGT_RUN_ID is the literal placeholder. Minted\n * by manager-worker via /host/runs/start at session-spawn time.\n * When unset, Claude leaves the literal placeholder (legacy behaviour);\n * cloud-broker startup fails and the agent must mint a run by hand.\n */\n runId?: string | null;\n /**\n * ENG-5371: IANA timezone (e.g. `Australia/Melbourne`) used to compute\n * the daily-session day boundary. When omitted, the daily-session\n * helper falls back to host-local (UTC on Lambda/EC2), which is the\n * pre-ENG-5371 behaviour. The manager resolves this from the same\n * source as ENG-5363's channel-MCP `TZ` env var (`teamSettings.timezone`).\n */\n agentTimezone?: string | null;\n log: (msg: string) => void;\n}\n\nexport interface PersistentSession {\n codeName: string;\n startedAt: number | null;\n restartCount: number;\n status: 'starting' | 'running' | 'stopped' | 'crashed';\n /**\n * ENG-4659: the UUID we passed to claude on the most recent spawn.\n * Set right after `tmux new-session` succeeds. This is the value the\n * recovery hook compares against `lastFailureSessionId` to detect\n * \"same UUID failed twice in a row\" — they used to be the same\n * field, which made the gate compare a value to itself and always\n * trip after the first failure (CodeRabbit catch).\n */\n currentSessionId: string | null;\n /**\n * ENG-4659: tail of the tmux pane the last time the session\n * transitioned to crashed. Captured by readPaneLogTail() when the\n * healthcheck detects no tmux session, so the next \"unhealthy\" log\n * line carries the actual error Claude printed before exiting.\n * Cleared on the next successful spawn.\n */\n lastFailureTail: string | null;\n /**\n * ENG-4659: the session UUID that was in flight when the previous\n * failure was captured. Used to detect \"same UUID failing repeatedly\"\n * — when the just-failed `currentSessionId` matches this, we\n * increment `consecutiveSameUuidFailures`; when they differ (or this\n * is null), we reset to 1.\n */\n lastFailureSessionId: string | null;\n /**\n * Count of consecutive failures with the same `lastFailureSessionId`.\n * Reset on a successful spawn or when the session UUID rotates. The\n * \"Session ID already in use\" rotation gate fires at >= 2 to avoid\n * losing today's history on a single flaky failure.\n */\n consecutiveSameUuidFailures: number;\n /**\n * ENG-5371: the agent's IANA timezone (mirrored from\n * PersistentSessionConfig.agentTimezone on the most recent spawn).\n * Persisted on the session object so `prepareForRespawn` can pass it\n * to `rotateDailySession` without re-resolving from team settings.\n * `null` keeps host-local behaviour (the original ENG-4642 default).\n */\n agentTimezone: string | null;\n}\n\nconst sessions = new Map<string, PersistentSession>();\n\n// ---------------------------------------------------------------------------\n// Pane-log capture (ENG-4659)\n//\n// The tmux child we spawn is detached (`new-session -d`) so its stdio is\n// closed before claude even prints. To capture claude's output for\n// post-mortem we call `tmux pipe-pane -o` immediately after creating the\n// session, redirecting all pane output to a per-agent log file. On\n// unhealthy detection we read the tail of that file and surface it in\n// the log + scan it for known failure signatures.\n// ---------------------------------------------------------------------------\n\nconst PANE_LOG_DIR = join(homedir(), '.augmented');\nconst PANE_TAIL_LINES = 20;\n\nexport function paneLogPath(codeName: string): string {\n return join(PANE_LOG_DIR, codeName, 'pane.log');\n}\n\nfunction setupPaneLog(tmuxSession: string, codeName: string, log: (msg: string) => void): void {\n const logPath = paneLogPath(codeName);\n try {\n mkdirSync(dirname(logPath), { recursive: true });\n // Append a spawn marker rather than truncating — the previous\n // crash's output is exactly what an operator opening the file\n // wants to see, so wiping it on every respawn defeats the\n // post-mortem use case (CodeRabbit catch). The in-memory tail\n // captured at unhealthy-detection time still uses the most recent\n // lines so logs reflect the *current* failure correctly; the\n // on-disk file is the long-form record.\n appendFileSync(\n logPath,\n `\\n--- spawn ${new Date().toISOString()} (session ${tmuxSession}) ---\\n`,\n 'utf-8',\n );\n // Quote the path for the shell-cat invocation tmux runs.\n execSync(\n `tmux pipe-pane -o -t ${tmuxSession} 'cat >> ${logPath.replace(/'/g, `'\\\\''`)}'`,\n { stdio: 'ignore' },\n );\n } catch (err) {\n // Pane logging is diagnostic-only. A failure here just means the\n // next unhealthy log line won't carry the tail — the session still\n // runs. Don't propagate.\n log(`[persistent-session] pipe-pane setup failed for '${codeName}': ${(err as Error).message}`);\n }\n}\n\nexport function readPaneLogTail(codeName: string, lines: number = PANE_TAIL_LINES): string | null {\n const logPath = paneLogPath(codeName);\n if (!existsSync(logPath)) return null;\n try {\n const raw = readFileSync(logPath, 'utf-8');\n if (!raw) return null;\n // Strip ANSI escape sequences so the captured tail is\n // human-readable in operator-facing logs.\n // eslint-disable-next-line no-control-regex\n const stripped = raw.replace(/\\x1b\\[[0-9;?]*[A-Za-z]/g, '');\n const all = stripped.split('\\n').filter((l) => l.length > 0);\n return all.slice(-lines).join('\\n');\n } catch {\n return null;\n }\n}\n\n/**\n * Detect known Claude failure signatures from the captured pane tail.\n * Right now we recognise just one — \"Session ID already in use\" — which\n * was responsible for the multi-hour scout outage that motivated this\n * code (see ENG-4659). Returns 'unknown' when the tail has no signal we\n * can act on.\n */\ntype FailureSignature = 'session_id_in_use' | 'unknown';\n\nfunction detectFailureSignature(tail: string | null): FailureSignature {\n if (!tail) return 'unknown';\n if (/Session ID .* is already in use/i.test(tail)) return 'session_id_in_use';\n return 'unknown';\n}\n\n/**\n * Pre-spawn recovery hook (ENG-4659). Called by the manager between\n * detecting an unhealthy session and respawning. Inspects the captured\n * pane tail and applies one of the known recovery actions:\n *\n * - `session_id_in_use` (>= 2 consecutive failures with the same\n * UUID): rotate the daily session UUID so the next spawn doesn't\n * hit the same Claude rejection.\n *\n * Returns a short human-readable summary of any action taken (or\n * `null` if no action was warranted), suitable for inclusion in the\n * \"Session unhealthy\" log line.\n */\nexport function prepareForRespawn(codeName: string): string | null {\n const session = sessions.get(codeName);\n if (!session) return null;\n const signature = detectFailureSignature(session.lastFailureTail);\n if (\n signature === 'session_id_in_use' &&\n session.consecutiveSameUuidFailures >= 2\n ) {\n // Capture the count BEFORE resetting so the operator-facing log\n // line carries the actual streak length. The original code reset\n // first and then read 0 (CodeRabbit catch).\n const failureCount = session.consecutiveSameUuidFailures;\n // ENG-5371: rotate at the agent's configured timezone day boundary\n // when one is set; falls back to host-local otherwise.\n const newId = rotateDailySession(\n codeName,\n new Date(),\n session.agentTimezone ?? undefined,\n );\n // Reset counter — fresh UUID, fresh slate.\n session.consecutiveSameUuidFailures = 0;\n session.lastFailureSessionId = null;\n return `rotated daily-session UUID to ${newId} after ${failureCount}+ \"Session ID already in use\" failures`;\n }\n return null;\n}\n\n/**\n * Read the captured pane tail + restart counter for the manager to\n * include in its unhealthy log. Read-only; doesn't mutate session\n * state.\n */\nexport function getLastFailureContext(codeName: string): {\n tail: string | null;\n signature: FailureSignature;\n consecutiveSameUuid: number;\n restartCount: number;\n} {\n const session = sessions.get(codeName);\n return {\n tail: session?.lastFailureTail ?? null,\n signature: detectFailureSignature(session?.lastFailureTail ?? null),\n consecutiveSameUuid: session?.consecutiveSameUuidFailures ?? 0,\n restartCount: session?.restartCount ?? 0,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Session lifecycle (tmux-based)\n// ---------------------------------------------------------------------------\n\nexport function startPersistentSession(config: PersistentSessionConfig): PersistentSession {\n const existing = sessions.get(config.codeName);\n if (existing && existing.status === 'running') {\n return existing;\n }\n\n // Backoff on repeated crashes\n const restartCount = existing?.restartCount ?? 0;\n if (existing?.status === 'crashed' && existing.startedAt) {\n const backoffMs = Math.min(5000 * Math.pow(2, restartCount), 60_000);\n if (Date.now() - existing.startedAt < backoffMs) {\n return existing;\n }\n }\n\n const session: PersistentSession = {\n codeName: config.codeName,\n startedAt: null,\n restartCount,\n status: 'starting',\n currentSessionId: existing?.currentSessionId ?? null,\n lastFailureTail: existing?.lastFailureTail ?? null,\n lastFailureSessionId: existing?.lastFailureSessionId ?? null,\n consecutiveSameUuidFailures: existing?.consecutiveSameUuidFailures ?? 0,\n agentTimezone: config.agentTimezone ?? null,\n };\n sessions.set(config.codeName, session);\n\n spawnSession(config, session);\n return session;\n}\n\nfunction spawnSession(config: PersistentSessionConfig, session: PersistentSession): void {\n const { codeName, projectDir, mcpConfigPath, claudeMdPath, channels, devChannels, apiHost, log } = config;\n const claudeAuthMode = config.claudeAuthMode ?? 'subscription';\n const tmuxSession = `agt-${codeName}`;\n\n log(`[persistent-session] Starting tmux session '${tmuxSession}' for '${codeName}' (auth=${claudeAuthMode})`);\n\n try {\n sanitizeMcpJson(mcpConfigPath, apiHost);\n\n // Also write acpx config for task injection\n writeAcpxConfig(config);\n\n // Kill any existing tmux session (clean slate)\n try {\n execSync(`tmux kill-session -t ${tmuxSession} 2>/dev/null`, { stdio: 'ignore' });\n } catch { /* no existing session */ }\n\n // When running as root, claude looks at $HOME/.claude/.credentials.json\n // Auth mode branch (mutually exclusive — never leave both channels armed):\n //\n // subscription: sync OAuth creds from /home/*/.claude into /root/.claude\n // (idempotent). Do NOT set ANTHROPIC_API_KEY in env.\n // api_key: DELETE any /root/.claude creds so claude can't fall\n // back to a stale OAuth session, then inject\n // ANTHROPIC_API_KEY into the spawn env below.\n //\n // Leaving both present is the \"confused deputy\" path: claude's internal\n // precedence between ANTHROPIC_API_KEY and OAuth has changed between\n // versions and is undocumented. Keep exactly one channel live.\n if (claudeAuthMode === 'subscription') {\n const credsSynced = syncClaudeCredsToRoot();\n if (!credsSynced && platform() === 'linux' && typeof process.getuid === 'function' && process.getuid() === 0) {\n log(`[persistent-session] No Claude Code credentials found under /root/.claude or /home/*. Pair via browser from the host page, or run 'claude /login' on the host.`);\n }\n } else {\n // api_key mode — purge subscription creds under the current user's\n // home. Previously this was hardcoded to /root/.claude, which missed\n // non-root runs and macOS dev setups — letting OAuth creds silently\n // override the api_key in those environments. homedir() is what\n // claude-code itself reads, so that's the directory to clear.\n const claudeDir = join(homedir(), '.claude');\n for (const filename of ['.credentials.json', 'credentials.json']) {\n const p = join(claudeDir, filename);\n if (existsSync(p)) {\n try {\n rmSync(p, { force: true });\n log(`[persistent-session] Removed ${p} (api_key mode active — preventing OAuth fallback)`);\n } catch { /* non-fatal */ }\n }\n }\n if (!config.anthropicApiKey) {\n log(`[persistent-session] api_key mode but no anthropicApiKey passed. Session will fail auth.`);\n }\n }\n\n // Build claude args\n const args: string[] = [];\n\n // ENG-5397: never --resume. Every spawn mints a fresh session UUID\n // and passes it via --session-id, so claude creates a brand-new\n // conversation each time. Continuity moves onto surfaces we own\n // (memory, kanban, channel pending-inbound, audit_log) and is\n // injected into the new session by the SessionStart hook.\n //\n // History: pre-5397 we pinned a UUID per agent per local-day and\n // passed --resume on every same-day respawn. That coupled us to\n // Claude Code's stored-transcript format + Anthropic's request shape\n // — a regression in either (e.g. ENG-5353's 400 role 'system' on\n // 2.1.139) poisoned the transcript and persisted across respawns,\n // taking agents silent for hours (Stirling on 2026-05-22 against a\n // pre-fix session-id, even after the wrapper fix shipped). Fresh\n // every spawn structurally eliminates that failure mode.\n const sessionId = randomUUID();\n args.push('--session-id', sessionId);\n log(`[persistent-session] Starting fresh session ${sessionId} for '${codeName}'`);\n\n // ENG-5431: advance the daily-session marker to today's date with the\n // UUID we just minted. The day-rollover detector in manager-worker.ts\n // reads `current.date` to decide whether to restart this session at\n // the day boundary; if we don't update it on every spawn, that check\n // keeps firing every supervisor tick (the marker stays at the last\n // date written by getOrCreateDailySession / rotateDailySession, which\n // ENG-5397 stopped calling on the spawn path). Writing here keeps the\n // marker in lockstep with the running session, so `isStaleForToday`\n // flips to false on the next tick instead of looping.\n //\n // The write is sync disk IO (atomic tmp+rename inside writeFile()).\n // Isolate it so an ENOSPC / EACCES doesn't take down the spawn flow\n // — the marker is bookkeeping, not on the critical path. Worst case\n // if it fails: next supervisor tick re-fires the day-rollover restart\n // (the bug this fix exists for), but that's strictly no worse than\n // pre-fix behaviour and self-heals the next time the write succeeds.\n try {\n markDailySessionSpawn(codeName, sessionId, new Date(), config.agentTimezone ?? undefined);\n } catch (err) {\n log(\n `[persistent-session] Failed to update daily-session marker for '${codeName}': ${(err as Error).message}`,\n );\n }\n\n if (channels.length > 0) args.push('--channels', ...channels);\n if (devChannels.length > 0) args.push('--dangerously-load-development-channels', ...devChannels);\n args.push('--mcp-config', mcpConfigPath);\n if (existsSync(claudeMdPath)) args.push('--system-prompt-file', claudeMdPath);\n // ENG-5631: pass the agent's model as a session-scoped --model alias.\n // This is the only mechanism that actually takes effect for subscription\n // agents — the model written into the per-agent settings.json isn't read\n // by Claude Code, and a non-interactive agent never runs `/model`. Omit\n // the flag for an empty/unknown model so Claude Code uses the tier default.\n const modelAlias = claudeModelAlias(config.primaryModel);\n if (modelAlias) args.push('--model', modelAlias);\n args.push('--allow-dangerously-skip-permissions');\n args.push('--dangerously-skip-permissions');\n args.push('--strict-mcp-config');\n args.push('--name', tmuxSession);\n\n // Restrict tools to only the agent's configured MCP servers + built-in tools.\n // Without this, agents inherit the user's personal MCPs (Gmail, Calendar, etc.)\n const mcpServerNames = collectMcpServerNames(mcpConfigPath);\n args.push('--allowedTools', buildAllowedTools(mcpServerNames));\n\n // NOTE: CLAUDE_CODE_SIMPLE=1 blocks account plugins BUT also breaks\n // channel auth (Slack/Telegram require claude.ai OAuth). Instead, rely on\n // --strict-mcp-config + --allowedTools for tool isolation. Account plugins\n // may appear in the tool list but --allowedTools prevents calling them.\n //\n // IS_SANDBOX=1 bypasses claude's refusal to run under root/sudo with\n // --dangerously-skip-permissions. Dedicated EC2 hosts running only\n // agent workloads are effectively sandboxed (org-scoped VPC, no inbound,\n // no other tenants). Without this, the tmux session exits immediately\n // with \"cannot be used with root/sudo privileges for security reasons\".\n //\n // ENG-4717: previously we read `.env.integrations` and inlined every\n // KEY=VALUE pair onto the bash command string we handed tmux. That\n // string is the long-running shell process's argv — anything in it is\n // visible via `ps -eo command` for the entire session lifetime, which\n // means tokens like XURL_API_KEY and GRANOLA_ACCESS_TOKEN leaked to\n // any user who could ps on the host. We now write a wrapper script\n // (mode 0700) that sources the env file inside the spawned shell and\n // exec's claude — same pattern as the ACP wrapper below. The argv\n // visible to ps is just `bash <wrapper>`; the secrets never cross the\n // command-line boundary.\n // Every spawn is a fresh session (ENG-5397), so the init prompt is\n // always sent. The SessionStart hook fires before this turn lands\n // and injects the agent's orientation context (today's memory,\n // open kanban, pending channel threads) — by the time the agent\n // responds \"Ready.\" it already knows where it left off.\n const initPrompt =\n 'You are now online. Say \"Ready.\" and wait for incoming messages. Do not run any tools or load any data until a message arrives.';\n const claudeBin = resolveClaudeBinary();\n const claudeArgsJoined = args\n .map(a => (a.includes(' ') || a.includes('*')) ? JSON.stringify(a) : a)\n .join(' ');\n\n const wrapperPath = writePersistentClaudeWrapper({\n projectDir,\n claudeBin,\n initPrompt,\n claudeArgsJoined,\n });\n\n // ANTHROPIC_API_KEY is passed via `tmux new-session -e` so it lands in\n // the session shell's env without ever appearing in the claude shell's\n // argv — `ps aux` on the long-running `bash -c \"claude ...\"` process\n // would otherwise expose the raw key for the session's lifetime.\n // The `-e` flag's exposure is bounded to the new-session invocation,\n // which exits in well under a second.\n const tmuxSessionEnvArgs: string[] = [];\n if (claudeAuthMode === 'api_key' && config.anthropicApiKey) {\n tmuxSessionEnvArgs.push('-e', `ANTHROPIC_API_KEY=${config.anthropicApiKey}`);\n }\n\n // The command tmux runs is just the wrapper path — no secrets, no\n // long token strings, nothing for ps to expose.\n const claudeCmd = JSON.stringify(wrapperPath);\n\n // ENG-4632: defensively backfill HOME/USER before tmux spawns its\n // shell. When the manager is launched via `aws ssm send-command`\n // (or any non-login init), process.env can lack HOME — tmux\n // inherits that, the agent's claude process can't find\n // ~/.claude/.credentials.json, and falls back to the interactive\n // login picker forever. The managerStartCommand also applies this,\n // but a missing HOME at the persistent-session boundary is a\n // belt-and-braces fail-closed point worth keeping.\n const tmuxEnv: NodeJS.ProcessEnv = {\n ...process.env,\n // Treat empty-string as missing too — `HOME=\"\"` makes ~ resolve\n // to cwd, which is the same broken outcome as no HOME, just\n // better hidden.\n HOME: (process.env.HOME?.trim()) || homedir(),\n USER: (process.env.USER?.trim()) || userInfo().username,\n };\n // ENG-5051: stamp AGT_RUN_ID so Claude Code's `${AGT_RUN_ID}`\n // substitution in .mcp.json resolves to a real run for child MCPs\n // (cloud-broker, etc). Without this the persistent session boots\n // with the literal placeholder leaking through and cloud-broker\n // fails its startup guard. Only set when manager-worker successfully\n // minted a run — empty/missing keeps the legacy behaviour\n // (placeholder leaks, agent must mint by hand).\n if (config.runId) {\n tmuxEnv['AGT_RUN_ID'] = config.runId;\n }\n\n // ENG-5901 (CodeRabbit #1731): fail-fast probe — one structured line\n // per `${VAR}` in the rendered .mcp.json that the *actual* spawn env\n // leaves unset or empty. Based on the real tmuxEnv (HOME/USER\n // backfill, AGT_RUN_ID stamp) plus the `-e`-injected\n // ANTHROPIC_API_KEY, overlaid with .env.integrations exactly as the\n // wrapper's `source` will do — not raw process.env, which would\n // false-positive on injected keys. Observational only; the silent\n // failure it catches is \"substitutes to empty → MCP boots → upstream\n // 401s → channel dies\".\n for (const f of probeMcpEnvSubstitution({\n mcpConfigPath,\n envIntegrationsPath: join(projectDir, '.env.integrations'),\n baseEnv: {\n ...tmuxEnv,\n ...(claudeAuthMode === 'api_key' && config.anthropicApiKey\n ? { ANTHROPIC_API_KEY: config.anthropicApiKey }\n : {}),\n },\n })) {\n log(`[persistent-session] ${formatMissingVar(f)} agent=${codeName}`);\n }\n\n // Start tmux session with claude in it\n const child = spawn('tmux', [\n 'new-session', '-d', '-s', tmuxSession, '-c', projectDir,\n ...tmuxSessionEnvArgs, claudeCmd,\n ], {\n cwd: projectDir,\n stdio: ['ignore', 'pipe', 'pipe'],\n env: tmuxEnv,\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n log(`[persistent-session] Failed to create tmux session for '${codeName}' (exit ${code})`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n return;\n }\n log(`[persistent-session] tmux session '${tmuxSession}' created for '${codeName}'`);\n\n // ENG-4659: redirect pane output to a log file so we can recover\n // claude's actual error message after the session dies (claude's\n // stderr is otherwise unreachable since the tmux child is detached\n // before claude even prints).\n setupPaneLog(tmuxSession, codeName, log);\n\n // Track which session UUID we just spawned with so the recovery\n // hook can detect \"same UUID failing repeatedly\" and rotate.\n // Note: currentSessionId (set on spawn) is distinct from\n // lastFailureSessionId (set on the *previous* failure). Comparing\n // them is what makes the rotation gate work.\n session.currentSessionId = sessionId;\n\n // Auto-accept startup dialogs. Kanban work is driven by the\n // manager-gated hybrid edge-trigger injection (ENG-5435, ENG-5662),\n // not an in-session /loop, so there's nothing to arm here.\n acceptDialogs(tmuxSession, codeName, log, config.primaryModel ?? null, sessionId).catch(() => {});\n });\n\n child.on('error', (err) => {\n log(`[persistent-session] Failed to start tmux for '${codeName}': ${err.message}`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n });\n\n session.startedAt = Date.now();\n session.status = 'running';\n session.restartCount = 0;\n } catch (err) {\n log(`[persistent-session] Failed to start session for '${codeName}': ${(err as Error).message}`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n }\n}\n\n// ENG-6017: dialog detection + dismissal moved to claude-dialogs.ts so the\n// channel-input-watchdog can share it without an import cycle. The detectors\n// are re-exported through `_internals` below to keep existing tests green.\n\n/**\n * Detect whether the session has actually spawned its MCP server\n * children — the only reliable signal that claude reached the running\n * REPL. tmux pane content alone can't distinguish \"Ready\" prompts\n * from a stuck splash screen, so we shell out to ps and look for\n * children of the claude process.\n *\n * ENG-4634: previously the helper logged 'Session ready' whenever the\n * pane had a `❯` not preceded by 'Enter to confirm' — but the login\n * picker also has a `❯` and would short-circuit out as ready. Verify\n * a real MCP child exists (slack-channel.js / direct-chat-channel.js\n * / etc.) before claiming success.\n */\nfunction hasMcpChildren(tmuxSession: string): boolean {\n try {\n // Find the claude process inside this tmux session by --name flag\n // (set when the manager launches claude — see spawnSession).\n const claudePidOut = execSync(\n `pgrep -f -- \"--name ${tmuxSession}\" 2>/dev/null || true`,\n { encoding: 'utf-8' },\n ).trim();\n if (!claudePidOut) return false;\n // pgrep can match multiple processes (the bash shell wrapping\n // claude, plus claude itself). We want the **claude** process —\n // its children are the MCP servers we're checking for. Process\n // ordering means the wrapper shell is the LOWER PID and claude\n // (forked after the shell parses its args) is HIGHER. Pick the\n // max so `pgrep -P` finds the MCP children, not the bash kids.\n const pids = claudePidOut.split('\\n').map((p) => Number(p)).filter((p) => p > 0);\n if (pids.length === 0) return false;\n const claudePid = Math.max(...pids);\n // List child processes; if any look like an MCP channel server,\n // we're in business.\n const childrenOut = execSync(\n `pgrep -P ${claudePid} 2>/dev/null || true`,\n { encoding: 'utf-8' },\n ).trim();\n if (!childrenOut) return false;\n const childPids = childrenOut.split('\\n').map((p) => p.trim()).filter(Boolean);\n for (const cp of childPids) {\n const cmdline = execSync(\n `cat /proc/${cp}/cmdline 2>/dev/null | tr '\\\\0' ' ' || ps -p ${cp} -o args= 2>/dev/null || true`,\n { encoding: 'utf-8' },\n );\n if (\n /slack-channel\\.js|telegram-channel\\.js|direct-chat-channel\\.js|composio_/i.test(cmdline)\n ) {\n return true;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\nasync function acceptDialogs(\n tmuxSession: string,\n codeName: string,\n log: (msg: string) => void,\n primaryModel: string | null = null,\n sessionId: string | null = null,\n): Promise<void> {\n // Track whether we've already surfaced the login-picker warning so\n // operators don't get one log line per polling iteration. The\n // picker won't dismiss itself — once we've reported it, just keep\n // probing for the eventual recovery (e.g. operator completes OAuth\n // out-of-band) without re-spamming the log.\n let loginPickerReported = false;\n\n // Login-picker iterations don't count against the dialog-dismissal\n // budget — the operator can take minutes to complete OAuth via the\n // Hosts page, and we want acceptDialogs to still be running to\n // dismiss the trust + bypass dialogs that follow. Track the two\n // kinds of iterations separately so a slow OAuth doesn't burn the\n // 30s budget meant for the post-pair dialog cascade. Cap login-\n // picker waits at 15 minutes total to avoid leaking a forever-\n // polling helper if the operator walks away.\n let dialogIterations = 0;\n const MAX_DIALOG_ITERATIONS = 15;\n let loginPickerIterations = 0;\n const MAX_LOGIN_PICKER_ITERATIONS = 450; // 450 * 2s = 15 min\n\n while (\n dialogIterations < MAX_DIALOG_ITERATIONS &&\n loginPickerIterations < MAX_LOGIN_PICKER_ITERATIONS\n ) {\n await new Promise((r) => setTimeout(r, 2000));\n try {\n const screen = execSync(`tmux capture-pane -t ${tmuxSession} -p 2>/dev/null`, { encoding: 'utf-8' });\n\n // ENG-4634: handle the login picker BEFORE any other dialog\n // pattern. The picker has a `❯` cursor that the generic exit\n // branch at the bottom of this loop would otherwise read as\n // \"Session ready\". Press no key — sending Enter would trigger\n // an OAuth flow that requires browser interaction the agent\n // can't complete. Surface a clear, parseable log line so\n // operators / monitoring can route the operator to the\n // Hosts page to complete pairing.\n if (isLoginPickerVisible(screen)) {\n if (!loginPickerReported) {\n log(`[persistent-session] CLAUDE LOGIN REQUIRED for '${codeName}' — agent cannot start until ~/.claude.json is provisioned. Pair via the Hosts page or run 'claude /login' on the host.`);\n loginPickerReported = true;\n }\n loginPickerIterations++;\n continue;\n }\n\n // Reached the dialog cascade — count this iteration against the\n // shorter budget.\n dialogIterations++;\n\n // ENG-6017: the dialog cascade (theme picker → trust → resume-mode →\n // dev channels → MCP confirm → bypass permissions → session feedback)\n // now lives in sweepDialogs() so the channel-input-watchdog and the\n // inject-time hygiene share the exact same recognition. Behaviour and\n // log wording are unchanged (the theme-picker branch still runs before\n // the generic `❯ no Enter to confirm` exit below, since picker rows\n // also render with `❯`).\n const dialogAction = sweepDialogs(screen);\n if (dialogAction) {\n await sendDialogKeys(tmuxSession, dialogAction);\n log(`[persistent-session] ${dialogAction.logMessage} for '${codeName}'`);\n continue;\n }\n if (screen.includes('❯') && !screen.includes('Enter to confirm')) {\n // ENG-4634: don't trust the pane alone. Verify at least one\n // MCP server child has actually been spawned by the claude\n // process before declaring the session ready — otherwise a\n // splash-screen-with-cursor false-positive can race the\n // login picker and leave the agent silently broken.\n if (hasMcpChildren(tmuxSession)) {\n log(`[persistent-session] Session ready for '${codeName}' — MCP servers spawned`);\n // ENG-5770: opt this session into Anthropic's fast-output mode when\n // the agent's primary_model carries the `[fast]` marker. The send\n // happens before any inbound message is injected so the first real\n // prompt of the session is already running in fast mode. Banner is\n // re-checked here to skip + warn if a silent downgrade landed the\n // session on Sonnet/Haiku instead of Opus.\n await maybeSendFastMode({\n tmuxSession,\n codeName,\n primaryModel,\n sessionId,\n screen,\n log,\n });\n break;\n }\n // Pane looks idle but no MCP children yet — claude may still\n // be initialising. Keep polling; the loop bound caps total\n // wait at 30s.\n }\n } catch { break; }\n }\n}\n\n// ---------------------------------------------------------------------------\n// ENG-5770: fast-mode `/fast` send. Called once per ready-banner detection\n// (boot + every respawn path that re-runs acceptDialogs). Skips when the\n// agent isn't on a `[fast]` variant or when the live banner shows a non-Opus\n// family — `/fast` is currently only valid on Opus 4.6/4.7 and a silent\n// model downgrade is the failure mode we want to surface, not paper over.\n// ---------------------------------------------------------------------------\n\ninterface FastModeContext {\n tmuxSession: string;\n codeName: string;\n primaryModel: string | null;\n sessionId: string | null;\n /** Pane capture that triggered the ready-detection; used to inspect the model banner. */\n screen: string;\n log: (msg: string) => void;\n}\n\n/**\n * Inspect the live tmux pane to decide whether `/fast` is safe to send and,\n * if so, deliver it. The decision is intentionally conservative — we only\n * send when the screen mentions `opus`, never when it shows another family,\n * and never when the agent isn't tagged `[fast]`. Failures are logged and\n * swallowed: a missing `/fast` is non-fatal and we don't want a bad capture\n * to crash the spawn path.\n */\nasync function maybeSendFastMode(ctx: FastModeContext): Promise<void> {\n if (!isClaudeFastMode(ctx.primaryModel)) return;\n\n const sid = ctx.sessionId ?? 'unknown';\n const banner = ctx.screen.toLowerCase();\n const hasOpus = banner.includes('opus');\n const hasNonOpus = banner.includes('sonnet') || banner.includes('haiku');\n\n // A pane that mentions a non-Opus family without also mentioning Opus is\n // the silent-downgrade case. Skip + warn so the operator can see why their\n // fast-mode selection didn't take effect.\n if (hasNonOpus && !hasOpus) {\n ctx.log(\n `[fast-mode] skip /fast for agent=${ctx.codeName} session=${sid} — banner shows non-Opus model`,\n );\n return;\n }\n\n // Banner doesn't name a family at all (unusual layout, partial capture).\n // Skip rather than guess; a missing /fast just costs the operator the\n // fast-mode speed-up on this respawn.\n if (!hasOpus) {\n ctx.log(\n `[fast-mode] skip /fast for agent=${ctx.codeName} session=${sid} — Opus not visible in banner`,\n );\n return;\n }\n\n const ok = sendToAgent(ctx.tmuxSession, '/fast');\n if (ok) {\n ctx.log(`[fast-mode] sent /fast for agent=${ctx.codeName} session=${sid}`);\n } else {\n ctx.log(`[fast-mode] failed to send /fast for agent=${ctx.codeName} session=${sid} — tmux send-keys errored`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Prompt-ready detection + tmux send (shared by the manager-gated hybrid\n// kanban-work inject, ENG-5435). The in-session `/loop kanban-work` arm\n// (ENG-5404) that previously lived here was removed in ENG-5662 — kanban work\n// is now driven solely by the hybrid edge-trigger.\n// ---------------------------------------------------------------------------\n\n/**\n * Wait until the Claude Code REPL prompt is ready to accept input.\n *\n * v1 is intentionally simple: poll the pane every 500ms for the `❯ ` prompt\n * marker, return true the first time we see it, return false on timeout.\n * `acceptDialogs()` is the upstream gate that handles dialogs/login pickers,\n * so by the time this runs the pane is either at the prompt or fully wedged.\n *\n * The 10s cap is well past the typical post-acceptDialogs settle time\n * (~1-2s in observation) and short enough that a bad spawn fails fast\n * instead of holding up the manager loop.\n */\nasync function waitForPromptReady(tmuxSession: string): Promise<boolean> {\n const deadline = Date.now() + 10_000;\n while (Date.now() < deadline) {\n try {\n // CodeRabbit PR #1275: execFileSync (not execSync) so tmuxSession\n // is passed as an argv entry rather than interpolated into a\n // shell string. Matches the safer pattern used in acceptDialogs\n // for ENG-5364's resume-mode dismiss path.\n const screen = execFileSync(\n 'tmux',\n ['capture-pane', '-t', tmuxSession, '-p'],\n { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] },\n );\n if (screen.includes('❯ ')) return true;\n } catch { /* session may not exist yet — retry */ }\n await new Promise((r) => setTimeout(r, 500));\n }\n return false;\n}\n\n/**\n * Type of the function used to deliver a command string to the agent's\n * tmux session (via `sendToAgent`). Exposed via the _internals.__setArmSender\n * test seam so unit tests can swap in a spy without touching tmux. The\n * sender returns true on success, false on any tmux/exec failure.\n */\ntype ArmSender = (tmuxSession: string, command: string) => boolean;\n\n// ENG-5793: delay between the text-send and the Enter-send in\n// defaultArmSender. Exposed as a const so future tuning has a single seam.\nexport const SEND_KEYS_ENTER_DELAY_MS = 50;\n\n/**\n * Synchronous millisecond sleep. defaultArmSender is itself synchronous\n * (returns a boolean, not a Promise) and its call sites assume that;\n * Atomics.wait on a transient SharedArrayBuffer is the standard Node\n * idiom for a blocking sleep without changing the function signature.\n * The delay is tiny (50ms) so the cost vs an event-loop sleep is\n * negligible in practice.\n */\nfunction sleepBlockingMs(ms: number): void {\n const view = new Int32Array(new SharedArrayBuffer(4));\n Atomics.wait(view, 0, 0, ms);\n}\n\nfunction defaultArmSender(tmuxSession: string, command: string): boolean {\n try {\n // ENG-5793: split the text and the Enter into two separate\n // `tmux send-keys` invocations.\n //\n // The previous shape was `send-keys -t <s> <text> Enter` in ONE call.\n // When tmux's `extended-keys-format` is `csi-u` (the default on\n // tmux 3.4+ and what AWS EC2 hosts inherit), tmux delivers the\n // whole call as a bracketed paste (the text is multi-byte) and\n // re-encodes the trailing CR (0x0D) inside the paste as a CSI-u\n // sequence (`ESC[13;1u`). Claude Code's bracketed-paste tokenizer\n // doesn't decode CSI-u sequences within paste brackets, so the\n // encoded carriage return is silently dropped. The text lands in\n // the input buffer perfectly readable, the cursor sits at `❯`,\n // the agent never sees a turn. See anthropics/claude-code#43169.\n //\n // Split-call shape:\n // 1. `send-keys -l <text>` — `-l` (literal) sends the text as raw\n // bytes without interpreting key names. tmux still brackets it\n // as a paste, but the paste-end marker fires when this call\n // returns, so the Enter that follows is OUTSIDE the paste.\n // 2. Short sleep — give Claude Code's TUI a tick to commit the\n // paste before the Enter arrives. Without this, the two\n // invocations can be batched into one input window on the\n // app's side, putting us back in the same paste-internal-CR\n // trap.\n // 3. `send-keys Enter` — sent as a standalone keystroke, NOT\n // inside a paste, so the `\\r` is delivered raw and Claude\n // Code treats it as \"submit current input\" as the Kitty\n // protocol spec mandates for the legacy Enter encoding.\n //\n // The old comment here claimed the two-step shape was needed so\n // Claude Code's REPL would parse `/` as a slash command — that\n // turned out to be incidental. Single-call `send-keys text Enter`\n // worked for slash commands only because they're short enough that\n // some tmux/Claude Code versions short-circuited the paste-wrap.\n // The csi-u-vs-paste-tokenizer interaction is the real reason\n // plain-text injects silently fail in the same call shape.\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, '-l', command], {\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n sleepBlockingMs(SEND_KEYS_ENTER_DELAY_MS);\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, 'Enter'], {\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n return true;\n } catch {\n return false;\n }\n}\n\nlet armSender: ArmSender = defaultArmSender;\n\n// ---------------------------------------------------------------------------\n// ENG-6017: inject-time pane hygiene (dialog sweep + orphan-input clear)\n// ---------------------------------------------------------------------------\n\n/**\n * Pane capture used by the pre-send hygiene. Returns null when the session\n * (or tmux itself) is unavailable — callers treat null as \"skip hygiene\",\n * which keeps unit tests and non-tmux environments fast and silent.\n * Swappable via _internals.__setPaneCapture for tests.\n */\ntype PaneCapture = (tmuxSession: string) => string | null;\n\nfunction defaultPaneCapture(tmuxSession: string): string | null {\n try {\n return execFileSync('tmux', ['capture-pane', '-t', tmuxSession, '-p'], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n timeout: 2_000,\n });\n } catch {\n return null;\n }\n}\n\nlet paneCapture: PaneCapture = defaultPaneCapture;\n\n/**\n * Key sender used by the pre-send hygiene (dialog dismissal + C-u clear).\n * One send-keys call per key — never batched, so multi-key sequences can't\n * be wrapped into a single bracketed paste. Swappable via\n * _internals.__setHygieneKeySender for tests.\n */\ntype HygieneKeySender = (\n tmuxSession: string,\n keys: readonly string[],\n interKeyDelayMs: number,\n) => Promise<void>;\n\nconst defaultHygieneKeySender: HygieneKeySender = async (tmuxSession, keys, interKeyDelayMs) => {\n for (let i = 0; i < keys.length; i++) {\n if (i > 0 && interKeyDelayMs > 0) {\n await new Promise((r) => setTimeout(r, interKeyDelayMs));\n }\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, keys[i]!], {\n stdio: 'ignore',\n });\n }\n};\n\nlet hygieneKeySender: HygieneKeySender = defaultHygieneKeySender;\n\n/**\n * ENG-6017: pre-send pane hygiene for the tmux send-keys fallback.\n *\n * Two failure modes observed live (koda, 2026-06-04) that make a blind\n * send-keys land wrong:\n *\n * 1. A dialog (e.g. Claude Code's session-feedback rating prompt)\n * overlays the REPL — the typed text and Enter go into the dialog,\n * not the input box.\n * 2. A previous injection's text is still sitting unsubmitted in the\n * input box — the new text would concatenate onto it, corrupting\n * both messages.\n *\n * So: capture the pane once; dismiss a recognised dialog if present\n * (default-deny — unknown overlays are logged, never keyed); then if the\n * input box still holds orphaned text, clear it with C-u (NEVER submit\n * it — the operator may have already re-sent a corrected version, and\n * blind-submitting a stale ghost instruction is worse than dropping it;\n * the content is logged hash-only per the prod logging policy, and the\n * channel-input-watchdog had its bounded chances to submit it first).\n *\n * Best-effort by design: any capture/send failure skips hygiene and lets\n * the send proceed — this layer must never block delivery. The whole\n * helper is ENG-5927-transitional: it dies with the send-keys fallback\n * when interactive agents move to verified acpx delivery.\n */\nasync function preSendPaneHygiene(\n tmuxSession: string,\n codeName: string,\n log: (msg: string) => void,\n): Promise<void> {\n try {\n let screen = paneCapture(tmuxSession);\n if (screen === null) return;\n\n const action = sweepDialogs(screen);\n if (action) {\n await hygieneKeySender(tmuxSession, action.keys, action.interKeyDelayMs);\n log(`[inject] ${action.logMessage} for '${codeName}' before injection`);\n // Give the TUI a beat to drop the overlay before re-reading the pane.\n await new Promise((r) => setTimeout(r, 300));\n screen = paneCapture(tmuxSession) ?? '';\n }\n\n const orphan = extractInputBoxText(screen);\n if (orphan) {\n log(\n `[inject] clearing orphaned input for '${codeName}' before injection (input_hash=${simpleTextHash(orphan)}, len=${orphan.length})`,\n );\n await hygieneKeySender(tmuxSession, ['C-u'], 0);\n }\n } catch {\n // Hygiene is best-effort — never block the actual send on it.\n }\n}\n\n/**\n * ENG-5435: thin wrapper exposing the tmux send-keys path to the\n * manager-worker (which now also injects prompts via the hybrid\n * kanban-work mode). Routes through the same `armSender` binding so\n * the existing `__setArmSender` test seam still intercepts manager-side\n * calls. Returns true on success, false on any tmux/exec failure.\n */\nexport function sendToAgent(tmuxSession: string, command: string): boolean {\n return armSender(tmuxSession, command);\n}\n\n/**\n * ENG-5435: exported so the manager-worker hybrid path can gate\n * injects on a ready prompt. Same 10s cap and `❯` detection as the\n * spawn-time arm uses. Kept on `_internals` too for existing tests.\n */\nexport async function isAgentPromptReady(tmuxSession: string): Promise<boolean> {\n return waitForPromptReady(tmuxSession);\n}\n\n// Exported for unit testing — see __tests__/persistent-session-dialogs.test.ts.\nexport const _internals = {\n isLoginPickerVisible,\n isResumeModeDialogVisible,\n detectFailureSignature,\n isClaudeProcessAliveInTmux,\n waitForPromptReady,\n // ENG-5770: exported so the unit test in claude-model-alias.test.ts can\n // exercise the send/skip decision without spawning a real tmux session.\n maybeSendFastMode,\n // Test seam: swap the tmux send-keys path so tests don't have to\n // spawn a real tmux server. ESM module-binding makes\n // `vi.spyOn(module, 'execFileSync')` ineffective for in-module\n // callers, so we route through this shim.\n __setArmSender(fn: ArmSender | null): void {\n armSender = fn ?? defaultArmSender;\n },\n // ENG-6017 test seam: swap the pane capture used by the inject-time\n // hygiene so unit tests can simulate dialog overlays / orphaned input\n // without a tmux server. null restores the real capture.\n __setPaneCapture(fn: PaneCapture | null): void {\n paneCapture = fn ?? defaultPaneCapture;\n },\n // ENG-6017 test seam: swap the hygiene key sender (dialog dismissal +\n // C-u clear) so unit tests can assert keystrokes without tmux.\n __setHygieneKeySender(fn: HygieneKeySender | null): void {\n hygieneKeySender = fn ?? defaultHygieneKeySender;\n },\n // ENG-5758 test seam: swap the acpx session probe so unit tests can\n // simulate \"session present\" / \"no session\" without spawning acpx.\n __setAcpxSessionProbe(fn: AcpxSessionProbe | null): void {\n acpxSessionProbe = fn ?? defaultAcpxSessionProbe;\n acpxSessionAvailableCache.clear();\n },\n // ENG-5758 test seam: pin the resolved acpx binary path so unit tests can\n // exercise the \"acpx present but no session\" branch deterministically,\n // independent of what's installed in the test node_modules. null re-resolves.\n __setAcpxBin(path: string | null): void {\n _acpxBin = path;\n },\n // Test-only resets so each test starts from a clean slate.\n __resetZombieState(): void {\n zombieProbeCache.clear();\n pendingZombieDetections.clear();\n },\n __resetAcpxSessionCache(): void {\n acpxSessionAvailableCache.clear();\n },\n __getSessionsMap(): Map<string, PersistentSession> {\n return sessions;\n },\n __peekPendingZombie(codeName: string): ZombieDetectionRecord | null {\n return pendingZombieDetections.get(codeName) ?? null;\n },\n};\n\n// ---------------------------------------------------------------------------\n// Task injection (acpx preferred, tmux send-keys fallback)\n// ---------------------------------------------------------------------------\n\n// ENG-5599: richer inject outcome so callers can tell \"confirmed delivery\"\n// (acpx exec) apart from \"sent but unverified\" (tmux send-keys fallback) apart\n// from \"not delivered at all\". The scheduled-task route needs this: a bare\n// `false` (which send-keys returns even after sending the keys) would make it\n// both nudge the live session AND spawn the legacy claude -p oneshot — double-\n// executing the task. `delivered` = confirmed; `fallbackUsed` = send-keys ran\n// (the keystroke landed, submission unverified); neither = genuine failure.\nexport interface InjectResult {\n delivered: boolean;\n fallbackUsed: boolean;\n}\n\n// Boolean-returning wrapper preserved for the many existing callers. `true`\n// still means \"confirmed delivery via acpx\" exactly as before (send-keys and\n// failure both collapse to false), so behaviour is unchanged for them.\nexport async function injectMessage(\n codeName: string,\n type: 'task' | 'chat' | 'system',\n content: string,\n meta?: Record<string, string>,\n log?: (msg: string) => void,\n): Promise<boolean> {\n return (await injectMessageWithStatus(codeName, type, content, meta, log)).delivered;\n}\n\nexport async function injectMessageWithStatus(\n codeName: string,\n type: 'task' | 'chat' | 'system',\n content: string,\n meta?: Record<string, string>,\n log?: (msg: string) => void,\n): Promise<InjectResult> {\n const _log = log ?? ((_: string) => {});\n const session = sessions.get(codeName);\n if (!session || session.status !== 'running') {\n _log(`[inject] SKIP '${codeName}' — session ${session ? `status=${session.status}` : 'not found in Map'}`);\n return { delivered: false, fallbackUsed: false };\n }\n\n const prefix = meta?.task_name ? `[Task: ${meta.task_name}] ` : '';\n const text = prefix + content;\n const projectDir = getProjectDir(codeName);\n\n // Preferred: use acpx exec for reliable injection (no paste issues) — but\n // ONLY when an acpx session actually exists for this agent. Without the\n // session gate, exec exits 4 (\"No acpx session found\") and, because the\n // spawn is detached/stdio:'ignore'/fire-and-forget, the failure is silent\n // and we'd report delivered:true while dropping the message (ENG-5758).\n const acpx = getAcpxBin();\n if (acpx && hasAcpxSession(acpx, projectDir, codeName)) {\n try {\n // Write prompt to temp file to avoid shell escaping issues\n const tmpDir = join(projectDir, '.claude');\n mkdirSync(tmpDir, { recursive: true });\n const tmpFile = join(tmpDir, '.agt-inject-prompt.txt');\n writeFileSync(tmpFile, text);\n\n _log(`[inject] acpx exec (fire-and-forget): cwd=${projectDir}, file=${tmpFile}`);\n const child = spawn(acpx, ['claude', 'exec', '-f', tmpFile], {\n cwd: projectDir,\n stdio: 'ignore',\n detached: true,\n });\n child.on('error', (err) => {\n _log(`[inject] acpx spawn error for '${codeName}': ${err.message}`);\n });\n // ENG-5758: surface non-zero exits — previously swallowed, which hid\n // the no-session failure. Invalidate the cache so the next inject\n // re-probes (the session may have gone away mid-flight).\n child.on('exit', (code) => {\n if (code && code !== 0) {\n _log(`[inject] acpx exec exited ${code} for '${codeName}' — message may not have been delivered`);\n acpxSessionAvailableCache.delete(codeName);\n }\n });\n child.unref();\n return { delivered: true, fallbackUsed: false };\n } catch (err) {\n _log(`[inject] acpx exec failed for '${codeName}': ${(err as Error).message}`);\n // Fall through to tmux\n }\n } else if (acpx) {\n _log(`[inject] no acpx session for '${codeName}' — falling back to tmux send-keys`);\n } else {\n _log(`[inject] acpx binary not found — falling back to tmux send-keys`);\n }\n\n // Fallback: tmux send-keys via the shared armSender seam (testable, and\n // submits with a trailing Enter — see defaultArmSender).\n //\n // ENG-5782: collapse newlines before handing to tmux. Multi-line content\n // triggers tmux's bracketed-paste wrapping when passed to send-keys; once\n // Claude Code's TUI sees the paste-start escape, the trailing `Enter`\n // argument is captured as a literal newline *within* the paste rather\n // than as a \"submit current input\" event. The result: the prompt lands\n // in the input buffer perfectly readable but is never sent. The acpx\n // path above doesn't have this problem (file-loaded user turn, no\n // keystroke layer), so callers can keep building multi-line content\n // (run-boundary markers on their own line, structured nudges, etc.)\n // and we adapt it here for the fallback. Inert markers like\n // <!-- agt-run:UUID --> remain parseable on a single line — see the\n // RUN_MARKER_RE regex which doesn't care about line position.\n const singleLineText = text.replace(/\\s*\\n+\\s*/g, ' ').trim();\n // ENG-6017: dismiss any dialog overlaying the REPL and clear orphaned\n // input-box text before typing, so the send can't be eaten by a dialog\n // or concatenate onto a previously-stuck message.\n await preSendPaneHygiene(`agt-${codeName}`, codeName, _log);\n const sent = sendToAgent(`agt-${codeName}`, singleLineText);\n if (sent) {\n // tmux send-keys doesn't guarantee submission, so it's not a *confirmed*\n // delivery (delivered:false) — but the keystroke did land, so fallbackUsed\n // is true. injectMessage() still collapses this to `false` for legacy\n // callers; callers that must not double-act (the scheduled-task route)\n // read fallbackUsed and treat it as \"reached the session, don't also spawn\n // the oneshot\".\n _log(`[inject] tmux send-keys sent for '${codeName}' — unverified (delivered=false, fallbackUsed=true)`);\n return { delivered: false, fallbackUsed: true };\n }\n _log(`[inject] tmux send-keys failed for '${codeName}'`);\n return { delivered: false, fallbackUsed: false };\n}\n\n// ---------------------------------------------------------------------------\n// Session management\n// ---------------------------------------------------------------------------\n\nexport function stopPersistentSession(codeName: string, log: (msg: string) => void): void {\n const session = sessions.get(codeName);\n if (!session) return;\n\n log(`[persistent-session] Stopping session for '${codeName}'`);\n session.status = 'stopped';\n\n try {\n execSync(`tmux kill-session -t agt-${codeName} 2>/dev/null`, { stdio: 'ignore' });\n } catch { /* session may already be dead */ }\n\n // Also close any acpx session\n try {\n const acpx = getAcpxBin();\n if (acpx) {\n execFileSync(acpx, ['claude', 'sessions', 'close', `agt-${codeName}`], {\n cwd: getProjectDir(codeName),\n timeout: 5_000,\n stdio: 'ignore',\n });\n }\n } catch { /* non-fatal */ }\n\n sessions.delete(codeName);\n // ENG-5758: drop the cached acpx-session probe so a respawned session is\n // re-probed rather than inheriting the dead session's verdict.\n acpxSessionAvailableCache.delete(codeName);\n\n // ENG-4808: claude exiting should take its child channel-MCP processes\n // (telegram-channel.js, slack-channel.js, direct-chat-channel.js) with\n // it, but in practice those children survive the parent — node's stdio\n // close-on-parent-exit isn't always honoured, especially when claude is\n // killed via tmux SIGHUP. Without an explicit reap, every restart leaks\n // a tree of long-pollers each holding the agent's bot token (observed\n // 6+ orphans on agt-aws-1 during the Vigil debugging session). Schedule\n // the reap after a short delay so claude has a chance to clean up its\n // own children — anything still alive after that is fair game.\n setTimeout(() => {\n reapOrphanChannelMcps({ log });\n }, 3_000).unref();\n}\n\nexport function getSessionState(codeName: string): PersistentSession | null {\n return sessions.get(codeName) ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Zombie detection (ENG-5391)\n//\n// Claude can die inside a live tmux pane — the shell prompt is left behind,\n// `tmux has-session` keeps reporting the session as alive, and the manager's\n// existing health check never trips. On alyve-host-1 (2026-05-21) this caused\n// ~3-7 hours of silent unresponsiveness on `dwight` before the tmux session\n// itself was eventually replaced.\n//\n// We close the gap by probing for the actual claude process inside each\n// \"healthy\" tmux session. The probe matches on `--name <tmuxSession>` —\n// the same flag the manager passes to claude at spawn (see line ~607),\n// reused successfully by `hasMcpChildren()`. When the probe says no claude\n// process is alive, we treat the session as crashed, kill the dead tmux\n// shell so the next spawn isn't blocked by \"duplicate session\", and stash\n// a record for manager-worker to ship as an audit_log event.\n//\n// Cost guard: `isSessionHealthy` is called many times per tick per agent.\n// A short TTL cache (ZOMBIE_PROBE_TTL_MS) keeps the pgrep call at one per\n// ~30s per agent — well inside the < 5 min detection target.\n//\n// Grace window: claude can take 10-30s to fully start. We don't probe\n// within the first ZOMBIE_STARTUP_GRACE_MS after spawn / re-discovery,\n// which avoids false positives during boot.\n// ---------------------------------------------------------------------------\n\nconst ZOMBIE_PROBE_TTL_MS = 30_000;\nconst ZOMBIE_STARTUP_GRACE_MS = 60_000;\n\ninterface ZombieProbeCacheEntry {\n at: number;\n alive: boolean;\n}\nconst zombieProbeCache = new Map<string, ZombieProbeCacheEntry>();\n\nexport interface ZombieDetectionRecord {\n codeName: string;\n tmuxSession: string;\n detectedAt: number;\n /** Last few lines of the pane log captured at detection time. */\n paneTail: string | null;\n}\nconst pendingZombieDetections = new Map<string, ZombieDetectionRecord>();\n\n// ENG-5832: the pgrep matching (ERE anchoring + the `--` option-terminator\n// guard) moved to @augmented/core `runtime/session-probe.ts` so the channel\n// servers reuse the exact same logic. The shared primitive is tri-state\n// (alive | dead | unknown) to stay safe to reuse on hosts without pgrep; the\n// manager only ever cared about the boolean \"is claude alive\", and treats\n// both 'dead' and 'unknown' as crashed — exactly the pre-extraction behaviour\n// (the old local copy returned false on any catch, ENOENT included).\nfunction isClaudeProcessAliveInTmux(tmuxSession: string): boolean {\n return probeClaudeProcessInTmux(tmuxSession) === 'alive';\n}\n\n/**\n * Pop the pending zombie-detection record for an agent, if any. Manager-\n * worker calls this in the unhealthy-session branch to emit an audit_log\n * event before respawning. Idempotent — returns null once consumed.\n */\nexport function takeZombieDetection(codeName: string): ZombieDetectionRecord | null {\n const record = pendingZombieDetections.get(codeName);\n if (record) pendingZombieDetections.delete(codeName);\n return record ?? null;\n}\n\n/**\n * Check if a persistent session is healthy.\n *\n * Two conditions both need to hold:\n * 1. The tmux session named `agt-<codeName>` exists (existing check).\n * 2. A claude process is actually running inside it (ENG-5391).\n *\n * Also detects sessions from previous manager runs (not in the Map).\n */\nexport function isSessionHealthy(codeName: string): boolean {\n const tmuxSession = `agt-${codeName}`;\n\n // Check if tmux session exists\n try {\n execSync(`tmux has-session -t ${tmuxSession} 2>/dev/null`, { stdio: 'ignore' });\n } catch {\n // tmux session doesn't exist — mark as crashed but don't increment\n // restartCount here (that happens in spawnSession on actual failure)\n const session = sessions.get(codeName);\n if (session && session.status === 'running') {\n session.status = 'crashed';\n // ENG-4659: capture the pane log tail BEFORE the next spawn\n // overwrites pane.log. Stash it on the session so the next\n // unhealthy log line + the prepareForRespawn recovery hook can\n // both read it. Also track consecutive failures with the same\n // UUID so we only rotate after >= 2 fails (one transient failure\n // doesn't lose today's history).\n session.lastFailureTail = readPaneLogTail(codeName);\n // The UUID just used to spawn (currentSessionId) vs the UUID that\n // last failed (lastFailureSessionId). When they match, we're\n // failing on the same UUID twice in a row — increment the gate.\n // When they differ (fresh spawn after rotation, first-ever\n // failure, etc.) reset to 1. Comparing lastFailureSessionId to\n // itself was the original CodeRabbit-caught bug.\n const failedUuid = session.currentSessionId;\n if (failedUuid && failedUuid === session.lastFailureSessionId) {\n session.consecutiveSameUuidFailures += 1;\n } else {\n session.consecutiveSameUuidFailures = 1;\n }\n session.lastFailureSessionId = failedUuid;\n }\n return false;\n }\n\n // tmux session exists — ensure it's tracked in the Map\n if (!sessions.has(codeName)) {\n sessions.set(codeName, {\n codeName,\n startedAt: Date.now(),\n restartCount: 0,\n status: 'running',\n currentSessionId: null,\n lastFailureTail: null,\n lastFailureSessionId: null,\n consecutiveSameUuidFailures: 0,\n agentTimezone: null,\n });\n }\n\n const session = sessions.get(codeName)!;\n if (session.status !== 'running') {\n session.status = 'running';\n }\n\n // ENG-5391: tmux session exists — verify a claude process is actually\n // running inside it. Skip during the startup grace window so we don't\n // false-positive on a session that's still booting.\n const startedAt = session.startedAt;\n const withinGrace =\n startedAt != null && (Date.now() - startedAt) < ZOMBIE_STARTUP_GRACE_MS;\n\n if (!withinGrace) {\n const cached = zombieProbeCache.get(codeName);\n const cacheFresh = cached !== undefined && (Date.now() - cached.at) < ZOMBIE_PROBE_TTL_MS;\n const claudeAlive = cacheFresh ? cached.alive : isClaudeProcessAliveInTmux(tmuxSession);\n if (!cacheFresh) {\n zombieProbeCache.set(codeName, { at: Date.now(), alive: claudeAlive });\n }\n\n if (!claudeAlive) {\n // Zombie state: tmux session lingers but claude is gone. Capture\n // the pane tail BEFORE we kill the session so the next \"unhealthy\"\n // log line has the same forensics the regular crash path gets.\n const paneTail = readPaneLogTail(codeName);\n\n // Tear down the dead tmux session so the next spawnSession's\n // `tmux new-session` doesn't fail with \"duplicate session\" and\n // wedge the agent in a permanent zombie.\n try {\n execFileSync('tmux', ['kill-session', '-t', tmuxSession], { stdio: 'ignore' });\n } catch {\n // Race: another caller (or `stopPersistentSession`) already killed\n // it. The end state we want is unchanged either way.\n }\n\n session.status = 'crashed';\n session.lastFailureTail = paneTail;\n // Same consecutive-failure bookkeeping the tmux-gone path does so\n // the rotation gate behaves consistently across both crash modes.\n const failedUuid = session.currentSessionId;\n if (failedUuid && failedUuid === session.lastFailureSessionId) {\n session.consecutiveSameUuidFailures += 1;\n } else {\n session.consecutiveSameUuidFailures = 1;\n }\n session.lastFailureSessionId = failedUuid;\n\n // Stash for manager-worker to forward as an audit_log event. Only\n // record on transition (don't overwrite an unconsumed record) so a\n // hot loop of unhealthy checks doesn't emit duplicate audit rows.\n if (!pendingZombieDetections.has(codeName)) {\n pendingZombieDetections.set(codeName, {\n codeName,\n tmuxSession,\n detectedAt: Date.now(),\n // Cap pane tail to keep the audit payload bounded.\n paneTail: paneTail ? paneTail.slice(-1000) : null,\n });\n }\n zombieProbeCache.delete(codeName);\n return false;\n }\n }\n\n return true;\n}\n\nexport function resetRestartCount(codeName: string): void {\n const session = sessions.get(codeName);\n if (session) session.restartCount = 0;\n}\n\n// ---------------------------------------------------------------------------\n// Diagnostics — collect session health info for remote debugging\n// ---------------------------------------------------------------------------\n\nexport interface SessionDiagnostics {\n codeName: string;\n status: 'running' | 'starting' | 'stopped' | 'crashed' | 'unknown';\n startedAt: string | null;\n restartCount: number;\n tmuxAlive: boolean;\n screenCapture: string | null; // last N lines from tmux pane\n launchArgs: string | null; // process args\n channelStatus: string | null; // extracted from screen capture\n}\n\nexport function collectDiagnostics(codeNames: string[]): SessionDiagnostics[] {\n return codeNames.map((codeName) => {\n const session = sessions.get(codeName);\n const tmuxSession = `agt-${codeName}`;\n let tmuxAlive = false;\n let screenCapture: string | null = null;\n let launchArgs: string | null = null;\n let channelStatus: string | null = null;\n\n // Check tmux session (execFileSync to avoid shell injection)\n try {\n execFileSync('tmux', ['has-session', '-t', tmuxSession], { stdio: 'ignore' });\n tmuxAlive = true;\n } catch { /* session doesn't exist */ }\n\n // Capture last 30 lines from tmux pane\n if (tmuxAlive) {\n try {\n screenCapture = execFileSync('tmux', ['capture-pane', '-t', tmuxSession, '-p', '-S', '-30'], {\n encoding: 'utf-8',\n timeout: 3000,\n }).trim();\n } catch { /* non-fatal */ }\n }\n\n // Get process args via ps (safe — no user input in command)\n try {\n const psOutput = execFileSync('ps', ['aux'], { encoding: 'utf-8', timeout: 3000 });\n const line = psOutput.split('\\n').find((l) => l.includes(`agt-${codeName}`) && !l.includes('grep'));\n if (line) {\n const match = line.match(/claude\\s+.*/);\n launchArgs = match ? match[0].slice(0, 500) : null;\n }\n } catch { /* non-fatal */ }\n\n // Extract channel status from screen capture.\n // Only check the last 5 lines for current state — startup errors\n // may linger in scroll history but the agent could be healthy now.\n if (screenCapture) {\n const recentLines = screenCapture.split('\\n').slice(-5).join('\\n');\n const isIdle = recentLines.includes('❯');\n\n if (isIdle) {\n // Agent is at prompt — channels are likely working\n // Check full capture for persistent errors only\n if (screenCapture.includes('Channels require claude.ai authentication')) {\n channelStatus = 'error: auth required';\n } else {\n channelStatus = 'ok';\n }\n } else if (recentLines.includes('CHANNEL_ERROR') || recentLines.includes('CLOSED')) {\n channelStatus = 'error: disconnected';\n } else if (recentLines.includes('no MCP server configured')) {\n channelStatus = 'error: MCP server not found';\n } else if (recentLines.includes('ignored')) {\n channelStatus = 'error: channels ignored';\n } else {\n channelStatus = 'ok';\n }\n }\n\n return {\n codeName,\n status: tmuxAlive\n ? (session?.status ?? 'running')\n : (session?.status === 'running' ? 'crashed' : session?.status ?? 'unknown'),\n startedAt: session?.startedAt ? new Date(session.startedAt).toISOString() : null,\n restartCount: session?.restartCount ?? 0,\n tmuxAlive,\n screenCapture: screenCapture ? screenCapture.slice(-2000) : null, // limit size\n launchArgs,\n channelStatus,\n };\n });\n}\n\nexport function stopAllSessions(log: (msg: string) => void): void {\n for (const codeName of sessions.keys()) {\n stopPersistentSession(codeName, log);\n }\n}\n\nexport async function stopAllSessionsAndWait(\n log: (msg: string) => void,\n opts: { timeoutMs: number },\n): Promise<void> {\n const codeNames = [...sessions.keys()];\n if (codeNames.length === 0) return;\n\n for (const codeName of codeNames) {\n stopPersistentSession(codeName, log);\n }\n\n await new Promise<void>((resolve) => setTimeout(resolve, Math.min(opts.timeoutMs, 2000)));\n}\n\nexport function getProjectDir(codeName: string): string {\n return join(homedir(), '.augmented', codeName, 'project');\n}\n\n// ---------------------------------------------------------------------------\n// acpx config (needed for prompt-based injection)\n// ---------------------------------------------------------------------------\n\nfunction writeAcpxConfig(config: PersistentSessionConfig): void {\n const {\n projectDir,\n mcpConfigPath,\n claudeMdPath,\n channels,\n devChannels,\n anthropicApiKey,\n } = config;\n const claudeAuthMode = config.claudeAuthMode ?? 'subscription';\n\n const claudeArgs: string[] = [];\n if (channels.length > 0) claudeArgs.push('--channels', ...channels);\n if (devChannels.length > 0) claudeArgs.push('--dangerously-load-development-channels', ...devChannels);\n claudeArgs.push('--mcp-config', mcpConfigPath);\n if (existsSync(claudeMdPath)) claudeArgs.push('--system-prompt-file', claudeMdPath);\n // ENG-5631: keep ACP-injected turns on the same model as the tmux session.\n const acpModelAlias = claudeModelAlias(config.primaryModel);\n if (acpModelAlias) claudeArgs.push('--model', acpModelAlias);\n claudeArgs.push('--allow-dangerously-skip-permissions');\n claudeArgs.push('--dangerously-skip-permissions');\n claudeArgs.push('--strict-mcp-config');\n\n // Tool isolation for acpx exec (same as tmux session)\n const mcpServerNames2 = collectMcpServerNames(mcpConfigPath);\n claudeArgs.push('--allowedTools', buildAllowedTools(mcpServerNames2));\n\n // Write a wrapper script that sources .env.integrations then runs the ACP\n // adapter. This avoids ENAMETOOLONG from inlining long tokens (e.g. Xero\n // JWTs) into the command string, and works around acpx not supporting an\n // `env` field on agent configs.\n const acpCmd = `npx -y @agentclientprotocol/claude-agent-acp ${claudeArgs.map(a => (a.includes(' ') || a.includes('*')) ? JSON.stringify(a) : a).join(' ')}`;\n const envIntegrationsPath = join(projectDir, '.env.integrations');\n const wrapperPath = join(projectDir, '.claude', 'acpx-agent.sh');\n const wrapperLines = ['#!/usr/bin/env bash'];\n if (existsSync(envIntegrationsPath)) {\n wrapperLines.push(`set -a`, `source ${JSON.stringify(envIntegrationsPath)}`, `set +a`);\n }\n // Mirror the tmux-session auth branch: when mode=api_key we've purged the\n // OAuth creds under /root/.claude, so ACP task injections (acpx) also need\n // ANTHROPIC_API_KEY or every injected task fails auth. JSON.stringify is\n // shell-safe under bash for sk-ant-* tokens (no $/` chars).\n if (claudeAuthMode === 'api_key' && anthropicApiKey) {\n wrapperLines.push(`export ANTHROPIC_API_KEY=${JSON.stringify(anthropicApiKey)}`);\n }\n wrapperLines.push(`exec ${acpCmd}`);\n mkdirSync(join(projectDir, '.claude'), { recursive: true });\n // ENG-4717: writeFileSync's mode option only applies on file\n // creation; chmodSync afterwards enforces 0755 on overwrites too.\n writeFileSync(wrapperPath, wrapperLines.join('\\n') + '\\n', { mode: 0o755 });\n chmodSync(wrapperPath, 0o755);\n\n const acpxConfig = {\n defaultAgent: 'claude',\n defaultPermissions: 'approve-all',\n agents: {\n claude: {\n command: wrapperPath,\n },\n },\n };\n\n writeFileSync(join(projectDir, '.acpxrc.json'), JSON.stringify(acpxConfig, null, 2));\n}\n","/**\n * Sanitize a Claude Code .mcp.json file for compatibility.\n *\n * Fixes:\n * 1. Relative proxy URLs (e.g., /mcp-proxy/...) — resolved to absolute if\n * apiHost is provided, otherwise removed.\n * 2. URL-based entries (type: \"sse\") — converted to mcp-remote stdio bridge\n * since Claude Code doesn't support SSE MCP servers natively.\n *\n * Returns true if the file was modified.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\n\nexport function sanitizeMcpJson(\n mcpConfigPath: string,\n apiHost?: string,\n): boolean {\n try {\n const mcpRaw = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));\n const servers = mcpRaw.mcpServers as Record<string, Record<string, unknown>> | undefined;\n if (!servers) return false;\n\n let changed = false;\n for (const [key, val] of Object.entries(servers)) {\n if (typeof val?.url !== 'string') continue;\n\n // Resolve relative URLs\n if (val.url.startsWith('/')) {\n if (apiHost) {\n val.url = `${apiHost}${val.url}`;\n changed = true;\n } else {\n delete servers[key];\n changed = true;\n continue;\n }\n }\n\n // ENG-5071: do NOT wrap URL-based entries that carry auth headers in\n // mcp-remote. mcp-remote can't pass headers through to the upstream\n // MCP server, so the conversion silently drops the Authorization\n // header and the call fails at runtime. claudecode/index.ts's\n // writeMcpServer explicitly emits the raw `{ url, headers }` shape\n // for this case (ENG-4694).\n //\n // ENG-5074: Claude Code's MCP schema additionally requires a `type`\n // field on URL-based entries — without it claude rejects the\n // config at startup (\"Does not adhere to MCP server configuration\n // schema\") and the tmux session exits inside a second, putting\n // the agent in a respawn loop. Existing on-disk files written by\n // the pre-ENG-5074 writer carry url+headers but no type — backfill\n // 'http' (Streamable HTTP, the default for OAuth-MCP integrations)\n // so the sanitizer self-heals these entries instead of leaving\n // them to fail in claude. New writes from the post-ENG-5074\n // writer already include the field, so this is a no-op for them.\n const headers = val.headers as Record<string, unknown> | undefined;\n if (headers && typeof headers === 'object' && Object.keys(headers).length > 0) {\n if (typeof val.type !== 'string') {\n val.type = 'http';\n changed = true;\n }\n continue;\n }\n\n // Convert URL-based entries to mcp-remote stdio bridge\n // Claude Code doesn't support type: \"sse\" natively\n const url = val.url as string;\n delete val.url;\n delete val.type;\n val.command = 'npx';\n val.args = ['-y', 'mcp-remote', url, '--allow-http'];\n changed = true;\n }\n\n if (changed) writeFileSync(mcpConfigPath, JSON.stringify(mcpRaw, null, 2));\n return changed;\n } catch {\n return false;\n }\n}\n","// Shared helper for building Claude Code's --allowedTools string (ENG-4487).\n//\n// The manager spawns claude in three modes: persistent tmux session, acpx\n// exec wrapper, and one-shot `claude -p` for scheduled tasks + webapp direct\n// chat. Each site used to hand-roll its own allowedTools list, which drifted:\n// the one-shot paths forgot Skill and Agent, so integration skills under\n// .claude/skills/integration-... were silently invisible during scheduled-task\n// execution. Agents produced apologetic \"no data sources connected\" outputs\n// when the skills were actually on disk and their API keys were in env\n// vars — they just couldn't call the Skill tool.\n//\n// Invariant: every Claude Code invocation the manager spawns must include\n// Skill, Agent, and ToolSearch. Their absence disables integration-skill\n// activation, subagent dispatch, and MCP tool binding respectively, all\n// without warning. Keep that list in one place so a new spawn site\n// physically cannot miss them.\n//\n// ENG-5926: ToolSearch added. Modern Claude Code surfaces MCP tools via\n// the deferred-tool registry — tools start as schemas in ToolSearch's\n// catalog and bind on first invocation. Without ToolSearch in\n// `--allowedTools`, the entire MCP toolchain is invisible to a\n// dispatched sub-agent. Don's empirical evidence 2026-06-03 on\n// agt-aws-1: every `mcp__*` call from a Task-dispatched sub-agent\n// returned \"No such tool available.\" despite the wildcards being in\n// the sub-agent's `tools:` allowlist line. ToolSearch was the\n// missing piece — parent uses it for first-tool-call binding,\n// sub-agents inherit nothing if they don't have it. The parent\n// session also benefits (its own MCP first-call binding goes through\n// the same path). Adding it on every spawn closes the entire ENG-5897\n// → ENG-5905 → ENG-5922 → ENG-5924 → ENG-5926 thread.\n\n// Order is stable for test snapshots.\nconst BASE_TOOLS = ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Agent', 'Skill', 'ToolSearch'] as const;\n\n// Build the comma-separated allowedTools string for a Claude Code spawn.\n// Each MCP server name becomes a wildcard pattern matching every tool that\n// server exposes; plus the nine base built-ins (incl. ToolSearch for MCP\n// lazy-load — see ENG-5926).\nexport function buildAllowedTools(mcpServerNames: readonly string[]): string {\n // Claude Code's allowedTools patterns use underscore-separated names; MCP\n // server IDs in .mcp.json can use hyphens (e.g. direct-chat), so normalise.\n const mcpPatterns = mcpServerNames.map((name) => `mcp__${name.replace(/-/g, '_')}__*`);\n return [...mcpPatterns, ...BASE_TOOLS].join(',');\n}\n","import { existsSync, readFileSync } from 'node:fs';\n\n/**\n * ENG-5901 (ADR-0018 Phase 1) — fail-fast probe for `${VAR}` substitution\n * gaps at MCP spawn time.\n *\n * `.mcp.json` carries `${VAR}` placeholders that Claude Code substitutes\n * from the spawn environment (ADR-0006). Two failure modes:\n *\n * - var **unset** with no `:-default`: Claude Code refuses to parse the\n * config at startup (loud, per the Claude Code MCP docs). Still worth\n * a structured line so the operator's first grep explains claude's\n * parse error.\n * - var **set but empty** (`FOO=` line in `.env.integrations`, or an\n * empty export): substitution \"succeeds\" with `\"\"`, the MCP boots,\n * the upstream API 401s, and the channel dies silently. This is the\n * case the probe exists for.\n *\n * The probe is **observational only** — it emits one structured stderr\n * line per gap and never blocks the spawn. Greppable signature\n * (documented in docs/operator/credential-migration-eng5898.md):\n *\n * [mcp-env-substitution] missing var=<NAME> server=<KEY> state=<unset|empty>\n *\n * Pure helpers + a thin fs wrapper; unit-testable without a spawn.\n */\n\n/**\n * Vars that are legitimately absent at probe time because a later layer\n * binds them (or deliberately leaves them unset):\n *\n * - AGT_RUN_ID: exported per-spawn by the manager for scheduled runs\n * (ENG-4561) and intentionally unset for sessions with no `runs` row\n * (the augmented bridge maps missing → null run id; ENG-5818).\n */\nexport const LATE_BOUND_VARS: ReadonlySet<string> = new Set(['AGT_RUN_ID']);\n\nexport interface MissingSubstitutionVar {\n varName: string;\n /** Server key in `mcpServers` whose entry references the var. */\n server: string;\n state: 'unset' | 'empty';\n}\n\n/**\n * `${VAR}` with no default. `${VAR:-default}` can't fail substitution, so\n * the probe ignores it (same rule the Claude Code docs imply: only a\n * defaultless reference to an unset var is fatal).\n */\nconst TEMPLATE_VAR_RE = /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\n\nfunction collectVarsFromValue(value: unknown, into: Set<string>): void {\n if (typeof value === 'string') {\n for (const m of value.matchAll(TEMPLATE_VAR_RE)) into.add(m[1]!);\n } else if (Array.isArray(value)) {\n for (const v of value) collectVarsFromValue(v, into);\n }\n}\n\n/**\n * Find every defaultless `${VAR}` referenced by each MCP server entry\n * (env values, headers values, url, command, args) whose value in `env`\n * is unset or empty/whitespace. Returns one finding per (server, var).\n */\nexport function findMissingSubstitutionVars(\n mcpConfig: unknown,\n env: Record<string, string | undefined>,\n): MissingSubstitutionVar[] {\n const findings: MissingSubstitutionVar[] = [];\n if (typeof mcpConfig !== 'object' || mcpConfig === null) return findings;\n const servers = (mcpConfig as { mcpServers?: Record<string, unknown> }).mcpServers;\n if (typeof servers !== 'object' || servers === null) return findings;\n\n for (const [server, raw] of Object.entries(servers)) {\n if (typeof raw !== 'object' || raw === null) continue;\n const entry = raw as Record<string, unknown>;\n const vars = new Set<string>();\n collectVarsFromValue(entry['command'], vars);\n collectVarsFromValue(entry['args'], vars);\n collectVarsFromValue(entry['url'], vars);\n for (const block of [entry['env'], entry['headers']]) {\n if (typeof block !== 'object' || block === null) continue;\n for (const v of Object.values(block)) collectVarsFromValue(v, vars);\n }\n for (const varName of vars) {\n if (LATE_BOUND_VARS.has(varName)) continue;\n const value = env[varName];\n if (value === undefined) {\n findings.push({ varName, server, state: 'unset' });\n } else if (value.trim() === '') {\n findings.push({ varName, server, state: 'empty' });\n }\n }\n }\n return findings;\n}\n\n/** The structured, secret-free stderr line. */\nexport function formatMissingVar(f: MissingSubstitutionVar): string {\n return `[mcp-env-substitution] missing var=${f.varName} server=${f.server} state=${f.state}`;\n}\n\n/**\n * Parse a `.env.integrations` body with the same semantics as the\n * scheduled-task loader in manager-worker (skip blanks/comments, split on\n * first `=`) plus shell-quote stripping: the writer shell-quotes values\n * (`shellQuote`) because the persistent path `source`s the file, so a\n * Node-side reader must undo `'...'` wrapping to see the real value.\n */\nexport function parseEnvIntegrations(content: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const line of content.split('\\n')) {\n if (!line || line.startsWith('#') || !line.includes('=')) continue;\n const eqIdx = line.indexOf('=');\n const key = line.slice(0, eqIdx);\n let value = line.slice(eqIdx + 1);\n if (value.length >= 2 && value.startsWith(\"'\") && value.endsWith(\"'\")) {\n // shellQuote wraps in single quotes and escapes embedded ones as\n // `'\\''` — reverse both so the probe sees what the shell would.\n value = value.slice(1, -1).replaceAll(\"'\\\\''\", \"'\");\n }\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Convenience wrapper for spawn sites: read the rendered `.mcp.json` and\n * (optionally) `.env.integrations`, overlay the env file onto `baseEnv`\n * (mirroring what the wrapper's `source` / the scheduled-task loader\n * does), and return the findings. Never throws — a probe must not be\n * able to break a spawn.\n */\nexport function probeMcpEnvSubstitution(args: {\n mcpConfigPath: string;\n envIntegrationsPath?: string;\n baseEnv: Record<string, string | undefined>;\n}): MissingSubstitutionVar[] {\n try {\n const config = JSON.parse(readFileSync(args.mcpConfigPath, 'utf-8'));\n let env = args.baseEnv;\n if (args.envIntegrationsPath && existsSync(args.envIntegrationsPath)) {\n env = {\n ...args.baseEnv,\n ...parseEnvIntegrations(readFileSync(args.envIntegrationsPath, 'utf-8')),\n };\n }\n return findMissingSubstitutionVars(config, env);\n } catch {\n return [];\n }\n}\n","/**\n * ENG-4642: per-agent / per-day Claude session pinning.\n *\n * The persistent-session manager kills the tmux session on every spawn\n * (clean slate) and starts a fresh `claude` invocation. Pre-this-module,\n * that meant a new conversation every restart — operators lost context\n * any time the manager respawned.\n *\n * Goal: each calendar day is a fresh conversation, but every spawn\n * inside that day reuses the same conversation. We achieve this by\n * generating a stable UUID up front (Claude CLI accepts\n * `--session-id <uuid>` for the first spawn, `--resume <uuid>` for\n * subsequent ones) and persisting it to a tiny per-agent JSON file.\n *\n * Storage: `~/.augmented/<codeName>/daily-session.json` — same root the\n * persistent-session manager already owns via getProjectDir(). Schema:\n *\n * { \"date\": \"YYYY-MM-DD\", \"sessionId\": \"<uuid>\", \"history\": [...] }\n *\n * `history` keeps the last few days' entries so an operator can debug\n * which session was bound to which day. We trim to 7 days so the file\n * doesn't grow unbounded.\n *\n * Day boundary: defaults to host-local date (server timezone). Callers\n * may pass an IANA timezone (e.g. `Australia/Melbourne`) and the\n * rollover will fire at that zone's midnight instead — see ENG-5371.\n * The manager passes the agent's resolved `agentTimezone` (same source\n * as ENG-5363's channel MCP `TZ` env var: `teamSettings.timezone`,\n * defaulting to UTC) so the daily rollover lines up with what an\n * operator in the agent's timezone calls \"today\".\n *\n * Failure mode: if the on-disk JSONL Claude writes for the resumed\n * session is missing (host moved, profile wiped, claude version\n * incompatibility), `--resume` would fail and the agent would land on\n * the login picker. Callers verify the JSONL exists via\n * `sessionFileExists()` before choosing `--resume`; if it's gone we\n * fall back to `--session-id` (treat the stored UUID as fresh, claude\n * will materialise the JSONL on first turn).\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nconst HISTORY_DAYS = 7;\n\ninterface DailySessionEntry {\n date: string; // YYYY-MM-DD\n sessionId: string; // UUID v4\n startedAt: string; // ISO 8601\n}\n\ninterface DailySessionFile {\n current: DailySessionEntry | null;\n history: DailySessionEntry[];\n}\n\nexport interface DailySessionResult {\n sessionId: string;\n /** `true` when this call generated a new UUID (first spawn of a new day or first ever). */\n isNew: boolean;\n}\n\nfunction profileDir(codeName: string): string {\n return join(homedir(), '.augmented', codeName);\n}\n\nfunction dailySessionPath(codeName: string): string {\n return join(profileDir(codeName), 'daily-session.json');\n}\n\nfunction todayLocalIso(now: Date = new Date(), timezone?: string): string {\n // ENG-5371: when an IANA timezone is supplied (e.g. Australia/Melbourne),\n // compute the date in that zone via Intl.DateTimeFormat. Falling back to\n // Date getters preserves the original host-local behaviour for callers\n // (and tests) that don't supply a timezone — important for\n // backward-compatibility with the ENG-4642 contract.\n if (timezone) {\n try {\n const fmt = new Intl.DateTimeFormat('en-CA', {\n timeZone: timezone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n });\n // en-CA renders as `YYYY-MM-DD` already — no parts assembly needed.\n // Wrapped in try/catch in case the timezone string is invalid, in\n // which case we fall through to host-local rather than throw.\n return fmt.format(now);\n } catch {\n // Invalid IANA zone — fall back to host-local.\n }\n }\n const y = now.getFullYear();\n const m = String(now.getMonth() + 1).padStart(2, '0');\n const d = String(now.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nfunction readFile(codeName: string): DailySessionFile {\n const path = dailySessionPath(codeName);\n if (!existsSync(path)) return { current: null, history: [] };\n try {\n const raw = readFileSync(path, 'utf-8');\n const parsed = JSON.parse(raw) as Partial<DailySessionFile>;\n return {\n current: parsed.current ?? null,\n history: Array.isArray(parsed.history) ? parsed.history : [],\n };\n } catch {\n // Corrupt file — start fresh rather than crashing the manager.\n return { current: null, history: [] };\n }\n}\n\nfunction writeFile(codeName: string, data: DailySessionFile): void {\n const dir = profileDir(codeName);\n mkdirSync(dir, { recursive: true });\n // Atomic write: tmp + rename. A reader catching us mid-writeFileSync\n // would otherwise see truncated JSON and the corrupt-file branch in\n // readFile() would silently treat the agent as fresh state, losing\n // today's UUID and forcing a rollover the operator didn't ask for.\n // PID + randomUUID in the tmp suffix so two managers (or a respawn\n // racing with its predecessor) can't collide on the temp path and\n // have one rename remove the file the other is about to rename.\n // Mirrors the pattern in restart-flags.ts.\n const finalPath = dailySessionPath(codeName);\n const tmpPath = `${finalPath}.${process.pid}.${randomUUID()}.tmp`;\n writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');\n renameSync(tmpPath, finalPath);\n}\n\nfunction trimHistory(\n history: DailySessionEntry[],\n now: Date,\n timezone?: string,\n): DailySessionEntry[] {\n // Keep newest first, drop entries older than HISTORY_DAYS by date.\n // Take the injected `now` so callers with a frozen clock (tests\n // walking the day forward) don't get inconsistent cutoffs against\n // `new Date()`. The cutoff is computed in the same timezone as the\n // current entry's date string so equality comparisons hold across DST.\n const cutoff = new Date(now);\n cutoff.setDate(cutoff.getDate() - HISTORY_DAYS);\n const cutoffIso = todayLocalIso(cutoff, timezone);\n return history.filter((h) => h.date >= cutoffIso).slice(0, HISTORY_DAYS);\n}\n\n/**\n * Resolve the session UUID this agent should use right now. Generates\n * (and persists) a new UUID on the first call of a new local day, or\n * when the file is missing/corrupt; otherwise returns the day's\n * existing UUID. Idempotent within the same day.\n *\n * Concurrency: the read-then-write here is not under a file lock.\n * In our deployment the manager runs supervised, one process per\n * host (`agt manager start --supervise` / runSupervisorLoop), so\n * concurrent invocation for the same `codeName` is bounded to the\n * sub-second respawn window when the supervisor restarts the\n * worker. The atomic tmp+rename in writeFile() guarantees we never\n * read torn JSON, so the worst-case under a respawn race is two\n * managers minting different UUIDs and one rename winning — both\n * processes converge on the winner's UUID on the next supervisor\n * tick (which re-reads the file). We've taken that trade-off\n * over a proper inter-process lock because a stale lockfile (from\n * a SIGKILL'd manager) would block all subsequent runs and need\n * its own recovery path; the lossy outcome of a UUID race is one\n * tick of conversation churn, not a permanent block.\n */\nexport function getOrCreateDailySession(\n codeName: string,\n now: Date = new Date(),\n timezone?: string,\n): DailySessionResult {\n const today = todayLocalIso(now, timezone);\n const file = readFile(codeName);\n\n if (file.current && file.current.date === today) {\n return { sessionId: file.current.sessionId, isNew: false };\n }\n\n // Roll over: yesterday's (or older) entry moves to history, new one\n // takes its place.\n const next: DailySessionEntry = {\n date: today,\n sessionId: randomUUID(),\n startedAt: now.toISOString(),\n };\n const history = trimHistory(\n [...(file.current ? [file.current] : []), ...file.history],\n now,\n timezone,\n );\n writeFile(codeName, { current: next, history });\n return { sessionId: next.sessionId, isNew: true };\n}\n\n/**\n * Record the UUID a caller just spawned with so the day-rollover\n * marker (`current.date`) advances to today.\n *\n * ENG-5431: post-ENG-5397 every spawn mints a fresh `randomUUID()` in\n * `spawnSession()` and bypasses `getOrCreateDailySession`. Without this\n * helper, `daily-session.json` is never written on spawn, so\n * `isStaleForToday()` keeps returning true once the previous\n * `current.date` falls behind — re-firing the day-rollover restart on\n * every supervisor tick. Calling this after each spawn keeps the\n * marker in lockstep with the actual running session.\n *\n * Idempotent: re-calling with the same (date, sessionId) is a no-op\n * write of the same content. Different sessionId on the same date\n * just overwrites `current.sessionId` (the old one moves to history).\n */\nexport function markDailySessionSpawn(\n codeName: string,\n sessionId: string,\n now: Date = new Date(),\n timezone?: string,\n): void {\n const today = todayLocalIso(now, timezone);\n const file = readFile(codeName);\n if (file.current && file.current.date === today && file.current.sessionId === sessionId) {\n return;\n }\n const next: DailySessionEntry = {\n date: today,\n sessionId,\n startedAt: now.toISOString(),\n };\n const history = trimHistory(\n [...(file.current ? [file.current] : []), ...file.history],\n now,\n timezone,\n );\n writeFile(codeName, { current: next, history });\n}\n\n/**\n * Reset the day's pin — used as a recovery hatch after `--resume` is\n * rejected by claude (corrupt state, version mismatch). Writes a new\n * UUID for today, demotes the old one to history.\n */\nexport function rotateDailySession(\n codeName: string,\n now: Date = new Date(),\n timezone?: string,\n): string {\n const today = todayLocalIso(now, timezone);\n const file = readFile(codeName);\n const next: DailySessionEntry = {\n date: today,\n sessionId: randomUUID(),\n startedAt: now.toISOString(),\n };\n const history = trimHistory(\n [...(file.current ? [file.current] : []), ...file.history],\n now,\n timezone,\n );\n writeFile(codeName, { current: next, history });\n return next.sessionId;\n}\n\n/**\n * Encode an absolute project dir the way Claude Code stores it under\n * ~/.claude/projects/. Claude collapses runs of `/` and `.` into single\n * `-` separators with a leading `-` (no separator at the start). The\n * earlier \"/ only\" encoder produced a stale path for any project dir\n * containing a `.` (e.g. `/root/.augmented/scout/project`), which made\n * sessionFileExists() return false even when the JSONL was on disk.\n *\n * Diagnosed live on prod scout (ENG-4659): the on-disk dir was\n * /root/.claude/projects/-root--augmented-scout-project/\n * but our encoder produced\n * /root/.claude/projects/-root-.augmented-scout-project/\n * — the dot in `.augmented` wasn't translated. Result: every \"is\n * there JSONL on disk\" check returned false, the manager fell back to\n * `--session-id` reuse, and Claude rejected the same UUID with\n * \"Session ID already in use\" forever.\n *\n * Empirical observations from /root/.claude/projects/ on a live host:\n * /usr/bin -> -usr-bin\n * /root/.augmented/scout/project -> -root--augmented-scout-project\n * Behaviour: `[/.]` -> `-`, with consecutive separators preserved\n * (the `/.` between `root/` and `.augmented` becomes `--`).\n */\nfunction encodeProjectPath(projectDir: string): string {\n return '-' + projectDir.replace(/^\\//, '').replace(/[/.]/g, '-');\n}\n\n/**\n * Check whether claude has actually written a session JSONL for this\n * UUID. If the file is missing the `--resume` would fail and put the\n * agent on the login picker; callers should fall back to `--session-id`\n * instead. See encodeProjectPath() for the encoding rules.\n */\nexport function sessionFileExists(\n projectDir: string,\n sessionId: string,\n): boolean {\n const path = join(\n homedir(),\n '.claude',\n 'projects',\n encodeProjectPath(projectDir),\n `${sessionId}.jsonl`,\n );\n return existsSync(path);\n}\n\n/**\n * Directory under ~/.claude/projects/ where Claude Code stores every session\n * transcript for the given project dir. All of an agent's sessions —\n * persistent respawns (each a fresh UUID post-ENG-5397), scheduled tasks, and\n * direct-chat invocations — share this one directory because they all run with\n * the same cwd (getProjectDir). The token-usage monitor enumerates it.\n */\nexport function sessionTranscriptDir(projectDir: string): string {\n return join(homedir(), '.claude', 'projects', encodeProjectPath(projectDir));\n}\n\nexport function sessionFilePath(projectDir: string, sessionId: string): string {\n return join(sessionTranscriptDir(projectDir), `${sessionId}.jsonl`);\n}\n\n/**\n * Is the agent's session JSONL idle — i.e. has it not been written for\n * at least `idleSeconds`? Claude appends to the file on every turn\n * (tool calls, assistant messages, user messages) so a stale mtime is\n * a reliable proxy for \"nothing in flight\". Returns true if the file\n * is missing (no in-flight work to interrupt) or if its mtime is\n * older than the threshold.\n *\n * Used by the scheduled-rollover gate so we don't kill a tmux session\n * mid-task at the day boundary — defer the rollover one tick at a\n * time until the agent is between turns.\n */\nexport function isAgentIdle(\n projectDir: string,\n sessionId: string,\n idleSeconds = 60,\n now: Date = new Date(),\n): boolean {\n const path = sessionFilePath(projectDir, sessionId);\n if (!existsSync(path)) return true;\n try {\n const mtimeMs = statSync(path).mtimeMs;\n return now.getTime() - mtimeMs >= idleSeconds * 1000;\n } catch {\n // stat failed (race, permissions). Treat as non-idle to err on the\n // side of NOT interrupting a possibly-running task.\n return false;\n }\n}\n\n/**\n * Cheap \"should we roll over?\" check for the supervisor tick. Reads\n * the persisted current entry and compares its date against today's.\n * Does NOT mint a new UUID — the caller decides what to do with the\n * answer (typically: kill the tmux session iff isAgentIdle is true,\n * letting the next tick respawn fresh via getOrCreateDailySession).\n */\nexport function isStaleForToday(\n codeName: string,\n now: Date = new Date(),\n timezone?: string,\n): boolean {\n const file = readFile(codeName);\n if (!file.current) return false; // never seeded — nothing to roll\n return file.current.date !== todayLocalIso(now, timezone);\n}\n\n/**\n * Read-only accessor for the current entry, returns null when the\n * file doesn't exist or has no current entry. Useful to grab the\n * sessionId for the idle check without triggering a roll-over write.\n */\nexport function peekCurrentSession(codeName: string): {\n date: string;\n sessionId: string;\n startedAt: string;\n} | null {\n return readFile(codeName).current;\n}\n\n// Exported for unit tests — keep the surface small.\nexport const _internals = { todayLocalIso, dailySessionPath, profileDir, encodeProjectPath };\n","/**\n * ENG-5832 — Host-side agent-session liveness probe.\n *\n * This is the framework-agnostic, READ-ONLY primitive that answers \"is the\n * agent's interactive Claude session actually alive on this host right now?\"\n * by inspecting the local tmux server and process table. It is deliberately\n * separate from `../liveness/agent-liveness.ts` (ENG-4862), which derives a\n * UI-facing liveness state from heartbeat timestamps reported to the API —\n * that one is pure and browser-safe; THIS one shells out to `tmux`/`pgrep`\n * and is therefore node-only (exposed via the `@augmented/core/runtime/...`\n * subpath, never re-exported from the package barrel).\n *\n * Two consumers share this code so the subtle pgrep matching (ERE anchoring\n * + the `--` option-terminator guard CodeRabbit flagged) lives in one place:\n * - apps/cli `persistent-session.ts` — the manager's zombie detector, which\n * wraps these primitives in stateful bookkeeping (it also *kills* a dead\n * tmux shell; that mutation stays in the CLI).\n * - packages/mcp channel servers — to decide whether a freshly-arrived\n * inbound can actually be answered before acking it (see ack-reaction.ts).\n *\n * Tri-state by design. `execFileSync` throws both when a tool reports \"no\n * match\" (a real negative) AND when the tool is missing or times out (we\n * simply don't know). Collapsing those to a boolean is what makes a probe\n * dangerous to reuse: a host without `tmux`/`pgrep` would look uniformly\n * \"dead\" and every caller would over-react. We return 'unknown' for the\n * can't-tell cases so callers fail safe.\n */\n\nimport { execFileSync } from 'node:child_process';\n\n/** `alive` = confirmed present, `dead` = confirmed absent, `unknown` = couldn't determine. */\nexport type ProbeState = 'alive' | 'dead' | 'unknown';\n\n/**\n * tmux session name the manager uses when spawning an agent\n * (see persistent-session.ts `spawnSession`: `tmux new-session -s agt-<codeName>`).\n */\nexport function agentTmuxSessionName(codeName: string): string {\n return `agt-${codeName}`;\n}\n\n/**\n * Escape a string for safe embedding in a pgrep ERE pattern. tmux session\n * names are ASCII (`agt-<codeName>`) in practice, but `code_name` is external\n * input so we defensively neutralise every ERE metachar.\n */\nexport function escapePgrepRegex(value: string): string {\n return value.replace(/[.[\\]{}()*+?^$|\\\\]/g, '\\\\$&');\n}\n\n/**\n * Is a Claude process actually running inside the named tmux session?\n *\n * Matches on the `--name <tmuxSession>` argv pair the manager passes to claude\n * at spawn — the same flag `hasMcpChildren()` reuses successfully.\n *\n * - exit 0 / output → 'alive'\n * - exit 1 (no match) → 'dead'\n * - pgrep missing (ENOENT) / timeout / other → 'unknown'\n */\nexport function probeClaudeProcessInTmux(tmuxSession: string): ProbeState {\n // pgrep -f treats the pattern as an unanchored ERE against the full command\n // line. Without an end-boundary, `--name agt-foo` would match a claude\n // running as `--name agt-foobar` and we'd report a dead session as alive\n // (CodeRabbit). Anchor on whitespace/EOL either side so only the exact\n // `--name <tmuxSession>` argv pair matches.\n const escapedSession = escapePgrepRegex(tmuxSession);\n const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;\n try {\n // `--` ends pgrep's option parsing — the pattern itself begins with\n // `--name`, which would otherwise be parsed as a flag and produce either a\n // \"usage\" error or a silent no-match (verified on macOS).\n const out = execFileSync('pgrep', ['-f', '--', pattern], {\n encoding: 'utf-8',\n timeout: 3_000,\n }).trim();\n return out.length > 0 ? 'alive' : 'dead';\n } catch (err) {\n // execFileSync throws on a non-zero exit. pgrep exits 1 specifically when\n // nothing matched — that's an authoritative 'dead'. ENOENT (no pgrep) or\n // any other status (timeout-kill, usage error) is genuinely 'unknown'.\n const e = err as NodeJS.ErrnoException & { status?: number | null };\n if (e?.code === 'ENOENT') return 'unknown';\n return e?.status === 1 ? 'dead' : 'unknown';\n }\n}\n\n/**\n * Does the named tmux session exist?\n *\n * - exit 0 → 'alive'\n * - tmux missing (ENOENT) → 'unknown'\n * - any other failure → 'dead' (session absent)\n *\n * NOTE on the \"no server running\" ambiguity: `tmux has-session` also exits\n * non-zero when no tmux server is running at all, which is indistinguishable\n * here from \"server up, session gone\". Callers that can't otherwise tell\n * whether the agent is even tmux-managed MUST gate on `process.env.TMUX`\n * (present ⇒ a server is definitely running ⇒ non-zero genuinely means the\n * session is gone). See `probeAgentSessionGated`.\n */\nexport function probeTmuxSession(tmuxSession: string): ProbeState {\n try {\n execFileSync('tmux', ['has-session', '-t', tmuxSession], {\n stdio: 'ignore',\n timeout: 3_000,\n });\n return 'alive';\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e?.code === 'ENOENT') return 'unknown';\n return 'dead';\n }\n}\n\nexport interface SessionLiveness {\n tmux: ProbeState;\n claude: ProbeState;\n}\n\n/**\n * Combined read-only probe of an agent's interactive session. The claude\n * probe only runs when tmux is 'alive' — if the session shell is gone the\n * process question is moot, so claude inherits the tmux verdict.\n */\nexport function probeAgentSession(codeName: string): SessionLiveness {\n const session = agentTmuxSessionName(codeName);\n const tmux = probeTmuxSession(session);\n const claude = tmux === 'alive' ? probeClaudeProcessInTmux(session) : tmux;\n return { tmux, claude };\n}\n\ninterface ProbeCacheEntry {\n at: number;\n value: SessionLiveness;\n}\nconst probeCache = new Map<string, ProbeCacheEntry>();\n\n/** Default TTL — keeps the pgrep/tmux calls to roughly one per this window per agent. */\nexport const SESSION_PROBE_TTL_MS = 15_000;\n\n/**\n * `probeAgentSession` with a short TTL cache so a burst of inbound messages\n * doesn't fork a `tmux`/`pgrep` pair per message. The CLI manager uses its own\n * 30s cache; channel servers see higher inbound rates, so the shorter default\n * here trades a little freshness for staying well inside the detection target.\n *\n * @param now injectable clock for tests.\n */\nexport function probeAgentSessionCached(\n codeName: string,\n ttlMs: number = SESSION_PROBE_TTL_MS,\n now: number = Date.now(),\n): SessionLiveness {\n const cached = probeCache.get(codeName);\n if (cached && now - cached.at < ttlMs) return cached.value;\n const value = probeAgentSession(codeName);\n probeCache.set(codeName, { at: now, value });\n return value;\n}\n\n/** Test seam: drop the probe cache so each case starts clean. */\nexport function __resetSessionProbeCache(): void {\n probeCache.clear();\n}\n","/**\n * ENG-6017: shared Claude Code TUI dialog detection + dismissal.\n *\n * Extracted from persistent-session.ts's acceptDialogs() cascade so the\n * dialog knowledge has exactly one home, consumable from:\n *\n * - `acceptDialogs()` in persistent-session.ts — the spawn-time loop that\n * walks an agent through the first-run dialog cascade (theme → trust →\n * MCP → bypass …).\n * - `channel-input-watchdog.ts` — the per-poll-cycle watchdog that fires\n * Enter at stuck channel input. Before ENG-6017 the watchdog was\n * dialog-blind: when Claude Code's session-feedback dialog (\"How is\n * Claude doing this session?\") overlaid the pane, the watchdog's\n * single-shot Enter went into the dialog instead of the input box and\n * the inbound message sat unsubmitted for 40+ minutes (koda,\n * 2026-06-04) while every health metric stayed green.\n * - `injectMessageWithStatus()` — pre-send pane hygiene on the tmux\n * send-keys fallback path.\n *\n * Living in its own module (rather than persistent-session.ts) breaks the\n * import cycle persistent-session → channel-input-watchdog →\n * persistent-session that a shared export would otherwise create.\n *\n * DEFAULT-DENY: `sweepDialogs()` only ever returns an action for an\n * explicitly recognised dialog. An unknown dialog gets `null` — never a\n * blind Enter — because a future dialog's default option could be\n * destructive. Callers that want visibility into unknown overlays should\n * log the pane themselves.\n */\n\nimport { execFileSync } from 'node:child_process';\n\n/**\n * A recognised dialog plus the keystrokes that dismiss it.\n *\n * `keys` are tmux key names sent as individual `tmux send-keys` calls —\n * never batched into one call, so a multi-byte sequence can't get wrapped\n * into a single bracketed paste (the CSI-u trap documented on\n * defaultArmSender in persistent-session.ts).\n */\nexport interface DialogAction {\n kind:\n | 'theme-picker'\n | 'folder-trust'\n | 'resume-mode'\n | 'dev-channels'\n | 'mcp-servers'\n | 'bypass-permissions'\n | 'session-feedback';\n /** tmux key names, one send-keys invocation each. */\n keys: readonly string[];\n /** Delay between consecutive key sends (selector dialogs need a beat\n * between picking an option and confirming it). */\n interKeyDelayMs: number;\n /** Past-tense log fragment, e.g. \"Auto-accepted theme picker\". Callers\n * append their own context (`for '<codeName>'` etc.). */\n logMessage: string;\n}\n\n/**\n * Detect whether Claude Code is showing the **login picker** dialog.\n *\n * ENG-4634: this dialog appears when ~/.claude.json is missing or\n * Claude Code can't validate the saved session. Pressing Enter on\n * the default (1. Claude account with subscription) kicks off a\n * browser-based OAuth flow that an unattended agent can't complete —\n * the helper used to fall through to the generic `❯ no Enter to\n * confirm` exit branch and declare the session \"ready\" while the\n * actual claude REPL was still on the picker. Without explicit\n * detection, every manager respawn would silently flip the agent\n * back to the picker and never recover.\n *\n * Pattern matches the literal option strings claude renders. Both\n * 'Claude account with subscription' and 'Anthropic Console account'\n * are present on the picker (and not in the post-login UI), so the\n * conjunction is unambiguous.\n *\n * NOT part of sweepDialogs() — the picker must never be keyed past\n * (it needs an operator OAuth); acceptDialogs() handles it specially.\n */\nexport function isLoginPickerVisible(screen: string): boolean {\n return (\n screen.includes('Select login method') ||\n (screen.includes('Claude account with subscription') &&\n screen.includes('Anthropic Console account'))\n );\n}\n\n/**\n * Detect Claude Code's resume-mode dialog (ENG-5364).\n *\n * On `claude --resume <uuid>` against a transcript large enough to\n * trigger Claude Code 2.1.x's context-management heuristic, the agent\n * lands on an interactive picker offering:\n *\n * ❯ 1. Resume from summary (recommended)\n * 2. Resume full session as-is\n * 3. Don't ask me again\n *\n * Without auto-dismissal the agent sits silently waiting for keyboard\n * input on every manager respawn — channel inbounds stack up while\n * health metrics stay green. Surfaced fleet-wide on 2026-05-20\n * (don/stirling/maven hit it on a single manager restart).\n *\n * Match on the conjunction of two distinct option strings so a\n * passing mention of \"Resume\" in a transcript doesn't false-positive.\n */\nexport function isResumeModeDialogVisible(screen: string): boolean {\n return (\n screen.includes('Resume from summary') &&\n screen.includes(\"Don't ask me again\")\n );\n}\n\n/**\n * Detect Claude Code's session-feedback dialog (ENG-6017).\n *\n * After some turns Claude Code renders an optional rating prompt:\n *\n * ● How is Claude doing this session? (optional)\n * 1: Bad 2: Fine 3: Good 0: Dismiss\n *\n * It waits for a digit — Enter does nothing — so any injected message\n * sits in the input box unsubmitted, and the channel-input-watchdog's\n * Enter is swallowed too. Observed live on koda (agt-aws-1) 2026-06-04:\n * an operator Slack DM sat typed-but-unsubmitted for 40+ minutes behind\n * this dialog while pane-activity / synthetic-probe / heartbeat all\n * stayed green.\n *\n * Match the question text together with the literal `0: Dismiss` option\n * so a transcript merely *quoting* the question doesn't false-positive.\n */\nexport function isSessionFeedbackDialogVisible(screen: string): boolean {\n return (\n screen.includes('How is Claude doing this session') &&\n screen.includes('0: Dismiss')\n );\n}\n\n/**\n * Single-pass dialog recognition. Returns the dismissal action for the\n * first recognised dialog on screen, or `null` when no known dialog is\n * visible (including for the login picker, which must never be keyed\n * past — see isLoginPickerVisible).\n *\n * Branch order mirrors the original acceptDialogs() cascade: the theme\n * picker check must run before any generic `❯`-based readiness logic in\n * callers, since picker rows also render with `❯`.\n */\nexport function sweepDialogs(screen: string): DialogAction | null {\n if (\n screen.includes('Choose the text style') ||\n (screen.includes('Dark mode') && screen.includes('Light mode'))\n ) {\n return {\n kind: 'theme-picker',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted theme picker',\n };\n }\n if (screen.includes('Yes, I trust this folder')) {\n return {\n kind: 'folder-trust',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted folder trust',\n };\n }\n // ENG-5364: picks option 3 (\"Don't ask me again\") which Claude Code\n // persists in config, so subsequent resumes skip the dialog entirely.\n if (isResumeModeDialogVisible(screen)) {\n return {\n kind: 'resume-mode',\n keys: ['3', 'Enter'],\n interKeyDelayMs: 300,\n logMessage: \"Auto-dismissed resume-mode dialog (picked 'Don't ask me again')\",\n };\n }\n if (screen.includes('I am using this for local development')) {\n return {\n kind: 'dev-channels',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted dev channels',\n };\n }\n if (screen.includes('Enter to confirm') && screen.includes('MCP')) {\n return {\n kind: 'mcp-servers',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted MCP servers',\n };\n }\n if (screen.includes('Yes, I accept') && screen.includes('Bypass Permissions')) {\n return {\n kind: 'bypass-permissions',\n keys: ['2', 'Enter'],\n interKeyDelayMs: 300,\n logMessage: 'Auto-accepted bypass permissions',\n };\n }\n // ENG-6017: the rating prompt acts on the bare digit — no Enter needed.\n if (isSessionFeedbackDialogVisible(screen)) {\n return {\n kind: 'session-feedback',\n keys: ['0'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-dismissed session-feedback dialog',\n };\n }\n return null;\n}\n\n/**\n * Send a DialogAction's keystrokes to a tmux session, one send-keys call\n * per key with the action's inter-key delay. execFileSync (not execSync)\n * so the session name is an argv entry rather than shell-interpolated.\n */\nexport async function sendDialogKeys(\n tmuxSession: string,\n action: DialogAction,\n): Promise<void> {\n for (let i = 0; i < action.keys.length; i++) {\n if (i > 0 && action.interKeyDelayMs > 0) {\n await new Promise((r) => setTimeout(r, action.interKeyDelayMs));\n }\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, action.keys[i]!], {\n stdio: 'ignore',\n });\n }\n}\n\n/**\n * Tiny non-cryptographic hash for hash-only logging of channel input\n * (prod logging policy: input may contain PII/secrets, so log hash+len,\n * never content). Shared by the watchdog and the inject-time hygiene.\n */\nexport function simpleTextHash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) {\n h = ((h << 5) - h + s.charCodeAt(i)) | 0;\n }\n return h.toString(16);\n}\n","/**\n * ENG-4705: Channel input watchdog.\n *\n * Symptom: an inbound Slack/Telegram/Direct-Chat message lands in the Claude\n * Code TUI input buffer (visible as `❯ <text>` between the input-box rule\n * lines) but the channel server's auto-submit doesn't fire — the text just\n * sits there until something sends Enter manually.\n *\n * The dispatcher pattern landed in ENG-4684 reduces the *frequency* (slow\n * requests get fanned out to a background subagent so the parent's listener\n * turn returns immediately), but doesn't fix the underlying race: when\n * triage decides a request is \"fast\" and the parent handles it inline, the\n * main turn still occupies the TUI for several seconds, and any channel\n * message that arrives during that window stacks in the input buffer\n * un-submitted.\n *\n * Workaround the ticket itself documents: `tmux send-keys -t <session>\n * Enter`. This watchdog automates that — every poll cycle it captures the\n * pane for each managed claude-code agent, looks for un-submitted text in\n * the input box, and fires Enter once the same text has been sitting there\n * unchanged for STUCK_THRESHOLD_MS.\n *\n * Safety:\n * - Skip the agent if a tmux client is attached (a human might be typing).\n * - Require the text to be unchanged for STUCK_THRESHOLD_MS — short windows\n * avoid racing the channel server's own (eventual) submit.\n * - Bounded retries per stuck buffer (ENG-6017): up to MAX_ENTER_FIRES\n * Enters, each spaced by the stuck threshold, then a loud give-up log.\n * (The original single-shot design is exactly what stranded koda on\n * 2026-06-04: the one Enter went into Claude Code's session-feedback\n * dialog, the buffer was marked resolved, and an operator Slack DM sat\n * unsubmitted for 40+ minutes while every health metric stayed green.)\n * - Dialog-aware (ENG-6017): if a recognised dialog overlays the pane,\n * dismiss it via sweepDialogs() instead of firing Enter into it. The\n * Enter budget is not consumed by dialog dismissals. Unknown dialogs\n * are default-deny — never keyed.\n * - Skip if Claude is actively rendering a spinner (`✻ <verb>ing…`); a\n * fresh send-keys Enter into a busy TUI is harmless but noisy in logs.\n */\n\nimport { sweepDialogs, simpleTextHash, type DialogAction } from './claude-dialogs.js';\n\nconst STUCK_THRESHOLD_MS = 5_000;\n// ENG-6017: how many Enters we'll fire at one unchanged stuck buffer before\n// giving up loudly. Each retry waits another stuckThreshold after the last.\nconst MAX_ENTER_FIRES = 3;\n// ENG-4716: when a tmux client is attached we don't skip the agent\n// outright (the original safety-first carve-out swallowed the steady-\n// state \"monitoring\" case operators care about). Instead we widen the\n// stable-buffer window so a human typing has more than 5s to finish\n// before the watchdog steps in. Active typing changes the buffer hash\n// every keystroke and keeps resetting `firstSeenAt`, so this only\n// matters when the buffer is *truly* stable.\nconst ATTACHED_STUCK_THRESHOLD_MS = 15_000;\nconst INPUT_BOX_DIVIDER = /^[─━]{10,}/;\nconst PROMPT_PREFIX = '❯ ';\n\nexport interface AgentInputState {\n /** Hash of the input-box text observed last poll. */\n lastInputHash: string;\n /** Wall-clock ms when this hash was first seen. */\n firstSeenAt: number;\n /** ENG-6017: how many Enters we've fired at this hash so far. */\n fires: number;\n /** Wall-clock ms of the most recent Enter fire (0 = never). */\n lastFireAt: number;\n /** Have we already emitted the give-up log line for this hash? */\n gaveUpLogged: boolean;\n}\n\nexport interface WatchdogIo {\n /** Snapshot of the agent's tmux pane (multiline). Empty / null if the session doesn't exist. */\n capturePane: (codeName: string) => string | null;\n /** Whether any tmux client is attached to the session right now. */\n isClientAttached: (codeName: string) => boolean;\n /** Send a single Enter keystroke to the agent's tmux session. */\n sendEnter: (codeName: string) => void;\n /**\n * ENG-6017: send a dialog-dismissal key sequence (one send-keys call per\n * key, `interKeyDelayMs` apart) to the agent's tmux session.\n */\n sendKeys: (codeName: string, keys: readonly string[], interKeyDelayMs: number) => void;\n /** Logger. */\n log: (msg: string) => void;\n /** Current wall-clock ms (injectable for tests). */\n now: () => number;\n}\n\nexport interface WatchdogConfig {\n /** ms a stuck buffer must persist before we fire Enter. Defaults to STUCK_THRESHOLD_MS. */\n stuckThresholdMs?: number;\n /**\n * ms a stuck buffer must persist before we fire Enter when a tmux\n * client is attached to the session. Higher than `stuckThresholdMs`\n * to give a human typer extra headroom. Defaults to\n * ATTACHED_STUCK_THRESHOLD_MS.\n */\n attachedStuckThresholdMs?: number;\n /**\n * ENG-6017: max Enters fired at one unchanged stuck buffer before the\n * loud give-up log. Defaults to MAX_ENTER_FIRES.\n */\n maxEnterFires?: number;\n}\n\n/**\n * Single-agent decision step — pure given the pane text + state. Returns\n * the next state and the action to take: dismiss a dialog, fire Enter,\n * emit the one-shot give-up log, or nothing.\n *\n * Exported for unit testing.\n */\nexport function decide(\n pane: string,\n prev: AgentInputState | undefined,\n now: number,\n config: WatchdogConfig = {},\n): {\n fire: boolean;\n /** ENG-6017: a recognised dialog overlays the pane — dismiss it instead\n * of firing Enter into it. The Enter budget is untouched. */\n dialog?: DialogAction;\n /** ENG-6017: the Enter budget for this hash is exhausted — emit the\n * give-up log exactly once. */\n gaveUp?: boolean;\n next: AgentInputState | undefined;\n} {\n const threshold = config.stuckThresholdMs ?? STUCK_THRESHOLD_MS;\n const maxFires = config.maxEnterFires ?? MAX_ENTER_FIRES;\n\n // ENG-6017: dialogs first. While a recognised dialog overlays the pane,\n // an Enter would land on the dialog (the koda incident: the session-\n // feedback prompt ate the watchdog's single shot). Dismiss it and leave\n // the stuck-input state untouched — next cycle sees the clean pane and\n // the normal Enter logic resumes. Unknown overlays fall through to the\n // standard path (default-deny: we never key what we don't recognise;\n // worst case the bounded Enters fire and the give-up log surfaces it).\n const dialogAction = sweepDialogs(pane);\n if (dialogAction) {\n return { fire: false, dialog: dialogAction, next: prev };\n }\n\n const inputText = extractInputBoxText(pane);\n if (!inputText) {\n return { fire: false, next: undefined };\n }\n\n if (isActivelyProcessing(pane)) {\n // Don't reset the timer — the input might still be stuck once Claude\n // finishes; we just don't fire while a spinner is live.\n return { fire: false, next: prev };\n }\n\n const hash = simpleTextHash(inputText);\n if (!prev || prev.lastInputHash !== hash) {\n return {\n fire: false,\n next: { lastInputHash: hash, firstSeenAt: now, fires: 0, lastFireAt: 0, gaveUpLogged: false },\n };\n }\n\n // ENG-6017: bounded retries replace the original single-shot `resolved`\n // flag. An Enter that lands while a dialog is mid-render (or is eaten by\n // a TUI mode hiccup) gets another chance, each spaced by the same stuck\n // threshold; after maxFires the watchdog surfaces a loud give-up line —\n // the signature monitoring hooks onto — instead of silently shrugging.\n if (prev.fires >= maxFires) {\n if (!prev.gaveUpLogged) {\n return { fire: false, gaveUp: true, next: { ...prev, gaveUpLogged: true } };\n }\n return { fire: false, next: prev };\n }\n\n const sinceLastAttempt = prev.fires === 0 ? now - prev.firstSeenAt : now - prev.lastFireAt;\n if (sinceLastAttempt < threshold) return { fire: false, next: prev };\n\n return {\n fire: true,\n next: { ...prev, fires: prev.fires + 1, lastFireAt: now },\n };\n}\n\n/**\n * Extract the contents of the Claude Code input box from a pane snapshot.\n * Returns null when the input box is empty or absent.\n *\n * The TUI bracketed input area looks like:\n * ────────── agt-bob ──\n * ❯ ping repro 2\n * ──────────────────────\n *\n * We accept any line starting with `❯ ` whose previous non-empty line is a\n * row of `─` characters (the top divider) — that's robust to width changes\n * and to the optional ` agt-<codeName> ` label embedded in the divider.\n *\n * Exported for unit testing.\n */\nexport function extractInputBoxText(pane: string): string | null {\n const lines = pane.split('\\n');\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i] ?? '';\n if (!line.startsWith(PROMPT_PREFIX)) continue;\n // Walk back to the most recent non-empty line; it must be a divider row.\n let j = i - 1;\n while (j >= 0 && (lines[j] ?? '').trim() === '') j--;\n if (j < 0) continue;\n if (!INPUT_BOX_DIVIDER.test((lines[j] ?? '').trim())) continue;\n const text = line.slice(PROMPT_PREFIX.length).trim();\n return text.length > 0 ? text : null;\n }\n return null;\n}\n\n/**\n * True when the pane shows a live spinner (`✻ Cogitating…`,\n * `✻ Crunching…`, etc.). Past tense forms like `✻ Cogitated for 7s` mean\n * the work has finished and we treat the agent as idle.\n *\n * Exported for unit testing.\n */\nexport function isActivelyProcessing(pane: string): boolean {\n // Search bottom-up for the most recent spinner line.\n const lines = pane.split('\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = (lines[i] ?? '').trim();\n if (!line.startsWith('✻')) continue;\n // Past tense: \"✻ Cogitated for 7s\", \"✻ Crunched for 51s\" — work done.\n if (/\\bfor\\s+\\d+s\\s*$/.test(line)) return false;\n // Present participle: \"✻ Cogitating…\", \"✻ Crunching…\" — still working.\n if (/\\b\\w+ing[…\\.]{0,3}\\s*$/i.test(line)) return true;\n // Ambiguous spinner line — don't block on it.\n return false;\n }\n return false;\n}\n\n/**\n * Run one watchdog pass over the given agents. Stateful: keeps an internal\n * map of per-agent input state across calls.\n */\nexport function checkChannelInputs(\n codeNames: readonly string[],\n io: WatchdogIo,\n config: WatchdogConfig = {},\n states: Map<string, AgentInputState> = sharedStates,\n): void {\n const live = new Set(codeNames);\n for (const codeName of codeNames) {\n try {\n checkOne(codeName, io, config, states);\n } catch (err) {\n io.log(`[channel-input-watchdog] '${codeName}': ${(err as Error).message}`);\n }\n }\n // Drop state for agents that are no longer in scope.\n for (const key of [...states.keys()]) {\n if (!live.has(key)) states.delete(key);\n }\n // Keep give-up counters in sync with live agents as well — a stale entry\n // would mis-attribute an old count if a code name is reused (CodeRabbit\n // on PR #1764).\n for (const key of [...giveUpCounts.keys()]) {\n if (!live.has(key)) giveUpCounts.delete(key);\n }\n}\n\nfunction checkOne(\n codeName: string,\n io: WatchdogIo,\n config: WatchdogConfig,\n states: Map<string, AgentInputState>,\n): void {\n const pane = io.capturePane(codeName);\n if (!pane) {\n states.delete(codeName);\n return;\n }\n\n // ENG-4716: don't skip outright when a client is attached — operators\n // routinely keep a tmux client open just to monitor agents, and the\n // original blanket skip ate the steady-state case. Widen the stable-\n // buffer threshold instead. Active typing changes the buffer hash on\n // every keystroke, so the unchanged-hash gate already covers the\n // \"don't fight the human\" concern.\n const attached = io.isClientAttached(codeName);\n const effectiveConfig: WatchdogConfig = attached\n ? {\n ...config,\n stuckThresholdMs:\n config.attachedStuckThresholdMs ?? ATTACHED_STUCK_THRESHOLD_MS,\n }\n : config;\n\n const prev = states.get(codeName);\n const { fire, dialog, gaveUp, next } = decide(pane, prev, io.now(), effectiveConfig);\n\n if (next === undefined) {\n states.delete(codeName);\n } else {\n states.set(codeName, next);\n }\n\n if (dialog) {\n // ENG-6017: a recognised dialog is blocking the input box — dismiss it\n // instead of firing Enter into it. Next cycle re-evaluates the clean pane.\n io.log(\n `[channel-input-watchdog] '${codeName}': ${dialog.logMessage} (dialog was blocking the input box)`,\n );\n io.sendKeys(codeName, dialog.keys, dialog.interKeyDelayMs);\n return;\n }\n\n // Log hash + length only — channel input may contain PII / secrets, so\n // prod logging stays hash-only per the project's logging policy.\n const text = extractInputBoxText(pane) ?? '';\n const hash = next?.lastInputHash ?? simpleTextHash(text);\n\n if (gaveUp) {\n // ENG-6017: loud, single-shot per hash. This line is the fast-detection\n // signature for \"input typed but unsubmittable\" — the failure mode every\n // upstream health metric is blind to (koda 2026-06-04).\n const maxFires = effectiveConfig.maxEnterFires ?? MAX_ENTER_FIRES;\n io.log(\n `[channel-input-watchdog] '${codeName}': GIVING UP after ${maxFires} Enter attempts — input remains unsubmitted (input_hash=${hash}, len=${text.length})`,\n );\n // Count the event for the responsiveness probe (InputStuckGiveUps\n // metric) so the give-up reaches CloudWatch on the next probe cycle,\n // not just the local log.\n giveUpCounts.set(codeName, (giveUpCounts.get(codeName) ?? 0) + 1);\n return;\n }\n\n if (fire) {\n const maxFires = effectiveConfig.maxEnterFires ?? MAX_ENTER_FIRES;\n io.log(\n `[channel-input-watchdog] '${codeName}': stuck channel input — firing Enter (attempt ${next?.fires ?? 1}/${maxFires}, input_hash=${hash}, len=${text.length})`,\n );\n io.sendEnter(codeName);\n }\n}\n\nconst sharedStates = new Map<string, AgentInputState>();\n\n// ENG-6017: per-agent count of give-up events since the last responsiveness\n// probe drained them. Consumed by the manager's probe cycle and shipped to\n// CloudWatch as `InputStuckGiveUps` — the fast-detection metric for the\n// \"typed but unsubmittable\" failure mode (each event means a channel message\n// sat in the input box through every bounded Enter retry).\nconst giveUpCounts = new Map<string, number>();\n\n/**\n * Drain the give-up counter for one agent (returns the count since the last\n * drain). Read-and-reset so each probe cycle reports only new events.\n */\nexport function takeWatchdogGiveUpCount(codeName: string): number {\n const count = giveUpCounts.get(codeName) ?? 0;\n giveUpCounts.delete(codeName);\n return count;\n}\n\n/** Test seam — clear the singleton map between tests. */\nexport function _resetSharedStatesForTests(): void {\n sharedStates.clear();\n giveUpCounts.clear();\n}\n"],"mappings":"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","names":["execFileSync","join","homedir","existsSync","readFileSync","writeFileSync","mkdirSync","readFileSync","randomUUID","existsSync","readFileSync","writeFileSync","existsSync","execFileSync","existsSync","join","mkdirSync","writeFileSync","readFileSync","execFileSync","homedir","randomUUID","session"]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/persistent-session.ts","../src/lib/mcp-sanitize.ts","../src/lib/claude-tools.ts","../src/lib/mcp-env-probe.ts","../src/lib/daily-session.ts","../../../packages/core/src/runtime/session-probe.ts","../src/lib/claude-dialogs.ts","../src/lib/channel-input-watchdog.ts"],"sourcesContent":["/**\n * Persistent session manager for Claude Code agents.\n *\n * Hybrid approach:\n * - **tmux** for the interactive session (channels like Slack/Telegram\n * require a real TTY that only tmux provides)\n * - **acpx** for task injection (reliable prompt delivery via --no-wait,\n * avoids the tmux send-keys paste-not-submitting issue)\n *\n * On manager restart, detects existing tmux sessions and reattaches\n * without creating duplicates.\n */\n\nimport { spawn, execSync, execFileSync, type ChildProcess } from 'node:child_process';\nimport { join, dirname } from 'node:path';\nimport { homedir, platform, userInfo } from 'node:os';\nimport { existsSync, readFileSync, readdirSync, writeFileSync, appendFileSync, mkdirSync, chmodSync, copyFileSync, rmSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { sanitizeMcpJson } from './mcp-sanitize.js';\nimport { buildAllowedTools } from './claude-tools.js';\nimport { probeMcpEnvSubstitution, formatMissingVar } from './mcp-env-probe.js';\nimport { claudeModelAlias, isClaudeFastMode } from './claude-model-alias.js';\nimport { reapOrphanChannelMcps } from './orphan-channel-mcp-reaper.js';\nimport { randomUUID } from 'node:crypto';\nimport { markDailySessionSpawn, rotateDailySession } from './daily-session.js';\n// ENG-5832: the tmux/pgrep zombie-probe primitives now live in\n// @augmented/core so the channel servers (packages/mcp) can share the exact\n// same pgrep matching when deciding whether an inbound can be answered. The\n// stateful bookkeeping + dead-session teardown below stays CLI-only.\nimport { probeClaudeProcessInTmux } from '@augmented/core/runtime/session-probe.js';\n// ENG-6017: shared dialog detection/dismissal (also consumed by the\n// channel-input-watchdog) plus input-box extraction for the inject-time\n// pane hygiene below.\nimport {\n isLoginPickerVisible,\n isResumeModeDialogVisible,\n sweepDialogs,\n sendDialogKeys,\n simpleTextHash,\n} from './claude-dialogs.js';\nimport { extractInputBoxText } from './channel-input-watchdog.js';\n\n/**\n * When running as root on Linux, the tmux-spawned claude process reads\n * ~/.claude/.credentials.json from /root. But operators log in via `claude\n * /login` as ssm-user or ec2-user, leaving creds under their own home.\n * Copy the first valid creds file into /root/.claude so claude (running as\n * root inside tmux) finds them. Idempotent — safe to call on every spawn.\n *\n * Returns true if a copy was made (or the file is already up to date),\n * false if no creds could be found at all.\n */\nfunction syncClaudeCredsToRoot(): boolean {\n if (platform() !== 'linux') return true;\n if (typeof process.getuid !== 'function' || process.getuid() !== 0) return true;\n\n // Fast path: pair-via-browser writes creds directly to /root/.claude\n // (the throwaway claude session runs as root). If they're already\n // there, no sync needed.\n for (const filename of ['.credentials.json', 'credentials.json']) {\n if (existsSync(join('/root/.claude', filename))) return true;\n }\n\n // Legacy path: an operator ran `claude /login` interactively as\n // ec2-user. Find any /home/*/.claude credentials and copy them up.\n let sourcePath: string | null = null;\n try {\n const entries = readdirSync('/home', { withFileTypes: true });\n outer: for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n // Both filenames Claude Code has historically used — keep in sync\n // with findClaudeCredentialsPaths() in claude-auth-detect.ts.\n for (const filename of ['.credentials.json', 'credentials.json']) {\n const candidate = join('/home', entry.name, '.claude', filename);\n if (existsSync(candidate)) {\n sourcePath = candidate;\n break outer;\n }\n }\n }\n } catch { /* no /home or unreadable — fall through */ }\n\n if (!sourcePath) return false;\n\n const targetDir = '/root/.claude';\n // Preserve source filename so the resulting file matches what claude's\n // reader expects (it accepts either '.credentials.json' or 'credentials.json').\n const sourceFilename = sourcePath.endsWith('credentials.json') && !sourcePath.endsWith('.credentials.json')\n ? 'credentials.json'\n : '.credentials.json';\n const targetPath = join(targetDir, sourceFilename);\n try {\n if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true, mode: 0o700 });\n copyFileSync(sourcePath, targetPath);\n chmodSync(targetPath, 0o600);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the claude binary to an absolute path. The manager runs under a\n * minimal PATH (cloud-init root env) that doesn't include\n * /home/linuxbrew/.linuxbrew/bin, so a bare `claude` reference in the tmux\n * shell fails immediately — session exits, manager sees it as \"unhealthy\",\n * restarts, loops forever.\n *\n * Cached at first call: claude's location doesn't change between cycles,\n * and `which` spawns aren't free.\n */\nlet cachedClaudePath: string | null = null;\nexport function resolveClaudeBinary(): string {\n if (cachedClaudePath) return cachedClaudePath;\n // Operator override: honour CLAUDE_PATH for non-standard installs.\n const override = process.env.CLAUDE_PATH;\n if (override && existsSync(override)) {\n cachedClaudePath = override;\n return override;\n }\n // Try PATH first — respects an operator's custom install.\n try {\n const out = execSync('which claude 2>/dev/null', { encoding: 'utf-8' }).trim();\n if (out && existsSync(out)) {\n cachedClaudePath = out;\n return out;\n }\n } catch { /* fall through to canonical paths */ }\n const candidates = [\n '/home/linuxbrew/.linuxbrew/bin/claude',\n '/opt/homebrew/bin/claude',\n '/usr/local/bin/claude',\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n cachedClaudePath = p;\n return p;\n }\n }\n // Last resort — let the shell fail so logs show the missing binary.\n return 'claude';\n}\n\n/**\n * ENG-4717: write the wrapper script that the persistent tmux session\n * exec's instead of putting `KEY=VALUE claude ...` directly on the\n * tmux command line. The wrapper sources `.env.integrations` (mode\n * 0600) inside the spawned shell, so secrets land in the exec'd\n * claude process's env without ever crossing the argv boundary that\n * `ps -eo command` reads from.\n *\n * Returns the wrapper path. Always overwrites — kept idempotent so\n * a manager respawn after a credential rotation picks up the new\n * .env.integrations contents on the next session start.\n *\n * Exported for unit tests; production callers go through startSession.\n */\nexport function writePersistentClaudeWrapper(args: {\n projectDir: string;\n claudeBin: string;\n initPrompt: string;\n claudeArgsJoined: string;\n}): string {\n const { projectDir, claudeBin, initPrompt, claudeArgsJoined } = args;\n const envIntegrationsPath = join(projectDir, '.env.integrations');\n const wrapperPath = join(projectDir, '.claude', 'persistent-claude.sh');\n const wrapperLines = [\n '#!/usr/bin/env bash',\n 'set -e',\n // IS_SANDBOX=1 lets claude run under root/sudo with\n // --dangerously-skip-permissions on dedicated EC2 hosts.\n 'export IS_SANDBOX=1',\n ];\n if (existsSync(envIntegrationsPath)) {\n // `set -a` exports every variable assigned by `source`; `set +a`\n // restores the prior state. Anything in .env.integrations becomes\n // an environment variable for the exec'd claude process.\n wrapperLines.push(\n 'set -a',\n `source ${JSON.stringify(envIntegrationsPath)}`,\n 'set +a',\n );\n }\n // ENG-5353: when initPrompt is empty (resumed session), omit the empty\n // positional entirely. Passing `\"\"` as a positional to claude would still\n // append a blank user turn to the resumed transcript and trip the same\n // upstream regression we're trying to avoid.\n const initPromptArg = initPrompt ? `${JSON.stringify(initPrompt)} ` : '';\n wrapperLines.push(\n `exec ${JSON.stringify(claudeBin)} ${initPromptArg}${claudeArgsJoined}`,\n );\n mkdirSync(join(projectDir, '.claude'), { recursive: true });\n // 0700: only the agent process owner can read/execute. The wrapper\n // doesn't contain secrets itself (it sources them from the 0600\n // file) but a hostile reader could still see which env vars get\n // loaded — enough leakage to lock down. The mode option on\n // writeFileSync is only honoured when the file is *created*, so we\n // chmodSync afterwards to enforce 0700 on overwrites too (the\n // common case after the first respawn).\n writeFileSync(wrapperPath, wrapperLines.join('\\n') + '\\n', { mode: 0o700 });\n chmodSync(wrapperPath, 0o700);\n return wrapperPath;\n}\n\n/**\n * Collect MCP server names from the project .mcp.json to build the\n * --allowedTools pattern for tool isolation.\n */\nfunction collectMcpServerNames(mcpConfigPath: string): string[] {\n if (!existsSync(mcpConfigPath)) return [];\n try {\n const data = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));\n const servers = data.mcpServers as Record<string, unknown> | undefined;\n return servers ? Object.keys(servers) : [];\n } catch {\n return [];\n }\n}\n\n// ---------------------------------------------------------------------------\n// acpx binary resolver (used for task injection only)\n// ---------------------------------------------------------------------------\n\nlet _acpxBin: string | null = null;\nfunction getAcpxBin(): string {\n if (_acpxBin) return _acpxBin;\n\n // Walk up from this file to find node_modules/.bin/acpx.\n // Covers: dev (src/lib → ../../node_modules), built (dist/lib → ../../node_modules),\n // and npm global install (lib/node_modules/@scope/pkg/dist/lib → ../../node_modules).\n const moduleDir = dirname(fileURLToPath(import.meta.url));\n let dir = moduleDir;\n for (let i = 0; i < 6; i++) {\n const candidate = join(dir, 'node_modules', '.bin', 'acpx');\n if (existsSync(candidate)) {\n _acpxBin = candidate;\n return _acpxBin;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n try {\n execSync('which acpx', { stdio: 'ignore' });\n _acpxBin = 'acpx';\n return _acpxBin;\n } catch {\n // acpx not available — injection will fall back to tmux send-keys\n return '';\n }\n}\n\n// ---------------------------------------------------------------------------\n// acpx session probe (ENG-5758)\n//\n// `acpx claude exec` requires an existing acpx session for the project. On\n// interactive-tmux agents (channels need a real TTY, so the session is tmux,\n// not acpx-spawned) no acpx session is ever created, so exec exits non-zero\n// (\"No acpx session found\", exit 4). Previously the inject path spawned acpx\n// fire-and-forget with stdio:'ignore' and reported delivered:true regardless —\n// so the failure was invisible and the send-keys fallback was never reached,\n// silently dropping every injected message (kanban /loop, scheduled tasks,\n// synthetic probes). Probe with `list-sessions` and gate the acpx path on it.\n// ---------------------------------------------------------------------------\n\ntype AcpxSessionProbe = (acpxBin: string, projectDir: string) => boolean;\n\nfunction defaultAcpxSessionProbe(acpxBin: string, projectDir: string): boolean {\n try {\n execFileSync(acpxBin, ['claude', 'list-sessions'], {\n cwd: projectDir,\n timeout: 5_000,\n stdio: 'ignore',\n });\n return true; // exit 0 — a session exists for this project\n } catch {\n return false; // non-zero (e.g. exit 4 \"No acpx session found\")\n }\n}\n\nlet acpxSessionProbe: AcpxSessionProbe = defaultAcpxSessionProbe;\nconst acpxSessionAvailableCache = new Map<string, boolean>();\n\nfunction hasAcpxSession(acpxBin: string, projectDir: string, codeName: string): boolean {\n const cached = acpxSessionAvailableCache.get(codeName);\n if (cached !== undefined) return cached;\n const available = acpxSessionProbe(acpxBin, projectDir);\n acpxSessionAvailableCache.set(codeName, available);\n return available;\n}\n\n// ---------------------------------------------------------------------------\n// Types and state\n// ---------------------------------------------------------------------------\n\nexport interface PersistentSessionConfig {\n codeName: string;\n agentId: string;\n projectDir: string;\n mcpConfigPath: string;\n claudeMdPath: string;\n channels: string[];\n devChannels: string[];\n apiHost?: string;\n /**\n * Operator-configured Claude Code auth mode. 'subscription' (default) runs\n * `syncClaudeCredsToRoot()` so claude finds OAuth creds under /root/.claude.\n * 'api_key' puts ANTHROPIC_API_KEY into the spawn env AND deletes any\n * stored OAuth creds so the two auth paths are mutually exclusive.\n */\n claudeAuthMode?: 'subscription' | 'api_key';\n /** Decrypted Anthropic API key. Only used when claudeAuthMode === 'api_key'. */\n anthropicApiKey?: string | null;\n /**\n * ENG-5631: the agent's resolved primary model as a full platform model\n * name (e.g. `claude-sonnet-4-6`, possibly with an `openrouter/anthropic/`\n * prefix). The launcher reduces it to a family alias via `claudeModelAlias`\n * and passes `--model <alias>` to the `claude` spawn — without this, a\n * subscription agent ignores its platform model setting and runs the auth\n * tier's default (Opus 4.7 on Max). When empty/unknown, no `--model` flag is\n * passed and Claude Code uses the tier default.\n */\n primaryModel?: string | null;\n /**\n * ENG-5051: per-session run UUID. When set, exported into the tmux\n * session env so Claude Code's ${AGT_RUN_ID} placeholder substitution\n * in .mcp.json resolves to a real run, unblocking the cloud-broker\n * MCP which 400s when AGT_RUN_ID is the literal placeholder. Minted\n * by manager-worker via /host/runs/start at session-spawn time.\n * When unset, Claude leaves the literal placeholder (legacy behaviour);\n * cloud-broker startup fails and the agent must mint a run by hand.\n */\n runId?: string | null;\n /**\n * ENG-5371: IANA timezone (e.g. `Australia/Melbourne`) used to compute\n * the daily-session day boundary. When omitted, the daily-session\n * helper falls back to host-local (UTC on Lambda/EC2), which is the\n * pre-ENG-5371 behaviour. The manager resolves this from the same\n * source as ENG-5363's channel-MCP `TZ` env var (`teamSettings.timezone`).\n */\n agentTimezone?: string | null;\n log: (msg: string) => void;\n}\n\nexport interface PersistentSession {\n codeName: string;\n startedAt: number | null;\n restartCount: number;\n status: 'starting' | 'running' | 'stopped' | 'crashed';\n /**\n * ENG-4659: the UUID we passed to claude on the most recent spawn.\n * Set right after `tmux new-session` succeeds. This is the value the\n * recovery hook compares against `lastFailureSessionId` to detect\n * \"same UUID failed twice in a row\" — they used to be the same\n * field, which made the gate compare a value to itself and always\n * trip after the first failure (CodeRabbit catch).\n */\n currentSessionId: string | null;\n /**\n * ENG-4659: tail of the tmux pane the last time the session\n * transitioned to crashed. Captured by readPaneLogTail() when the\n * healthcheck detects no tmux session, so the next \"unhealthy\" log\n * line carries the actual error Claude printed before exiting.\n * Cleared on the next successful spawn.\n */\n lastFailureTail: string | null;\n /**\n * ENG-4659: the session UUID that was in flight when the previous\n * failure was captured. Used to detect \"same UUID failing repeatedly\"\n * — when the just-failed `currentSessionId` matches this, we\n * increment `consecutiveSameUuidFailures`; when they differ (or this\n * is null), we reset to 1.\n */\n lastFailureSessionId: string | null;\n /**\n * Count of consecutive failures with the same `lastFailureSessionId`.\n * Reset on a successful spawn or when the session UUID rotates. The\n * \"Session ID already in use\" rotation gate fires at >= 2 to avoid\n * losing today's history on a single flaky failure.\n */\n consecutiveSameUuidFailures: number;\n /**\n * ENG-5371: the agent's IANA timezone (mirrored from\n * PersistentSessionConfig.agentTimezone on the most recent spawn).\n * Persisted on the session object so `prepareForRespawn` can pass it\n * to `rotateDailySession` without re-resolving from team settings.\n * `null` keeps host-local behaviour (the original ENG-4642 default).\n */\n agentTimezone: string | null;\n}\n\nconst sessions = new Map<string, PersistentSession>();\n\n// ---------------------------------------------------------------------------\n// Pane-log capture (ENG-4659)\n//\n// The tmux child we spawn is detached (`new-session -d`) so its stdio is\n// closed before claude even prints. To capture claude's output for\n// post-mortem we call `tmux pipe-pane -o` immediately after creating the\n// session, redirecting all pane output to a per-agent log file. On\n// unhealthy detection we read the tail of that file and surface it in\n// the log + scan it for known failure signatures.\n// ---------------------------------------------------------------------------\n\nconst PANE_LOG_DIR = join(homedir(), '.augmented');\nconst PANE_TAIL_LINES = 20;\n\nexport function paneLogPath(codeName: string): string {\n return join(PANE_LOG_DIR, codeName, 'pane.log');\n}\n\nfunction setupPaneLog(tmuxSession: string, codeName: string, log: (msg: string) => void): void {\n const logPath = paneLogPath(codeName);\n try {\n mkdirSync(dirname(logPath), { recursive: true });\n // Append a spawn marker rather than truncating — the previous\n // crash's output is exactly what an operator opening the file\n // wants to see, so wiping it on every respawn defeats the\n // post-mortem use case (CodeRabbit catch). The in-memory tail\n // captured at unhealthy-detection time still uses the most recent\n // lines so logs reflect the *current* failure correctly; the\n // on-disk file is the long-form record.\n appendFileSync(\n logPath,\n `\\n--- spawn ${new Date().toISOString()} (session ${tmuxSession}) ---\\n`,\n 'utf-8',\n );\n // Quote the path for the shell-cat invocation tmux runs.\n execSync(\n `tmux pipe-pane -o -t ${tmuxSession} 'cat >> ${logPath.replace(/'/g, `'\\\\''`)}'`,\n { stdio: 'ignore' },\n );\n } catch (err) {\n // Pane logging is diagnostic-only. A failure here just means the\n // next unhealthy log line won't carry the tail — the session still\n // runs. Don't propagate.\n log(`[persistent-session] pipe-pane setup failed for '${codeName}': ${(err as Error).message}`);\n }\n}\n\nexport function readPaneLogTail(codeName: string, lines: number = PANE_TAIL_LINES): string | null {\n const logPath = paneLogPath(codeName);\n if (!existsSync(logPath)) return null;\n try {\n const raw = readFileSync(logPath, 'utf-8');\n if (!raw) return null;\n // Strip ANSI escape sequences so the captured tail is\n // human-readable in operator-facing logs.\n // eslint-disable-next-line no-control-regex\n const stripped = raw.replace(/\\x1b\\[[0-9;?]*[A-Za-z]/g, '');\n const all = stripped.split('\\n').filter((l) => l.length > 0);\n return all.slice(-lines).join('\\n');\n } catch {\n return null;\n }\n}\n\n/**\n * Detect known Claude failure signatures from the captured pane tail.\n * Right now we recognise just one — \"Session ID already in use\" — which\n * was responsible for the multi-hour scout outage that motivated this\n * code (see ENG-4659). Returns 'unknown' when the tail has no signal we\n * can act on.\n */\ntype FailureSignature = 'session_id_in_use' | 'unknown';\n\nfunction detectFailureSignature(tail: string | null): FailureSignature {\n if (!tail) return 'unknown';\n if (/Session ID .* is already in use/i.test(tail)) return 'session_id_in_use';\n return 'unknown';\n}\n\n/**\n * Pre-spawn recovery hook (ENG-4659). Called by the manager between\n * detecting an unhealthy session and respawning. Inspects the captured\n * pane tail and applies one of the known recovery actions:\n *\n * - `session_id_in_use` (>= 2 consecutive failures with the same\n * UUID): rotate the daily session UUID so the next spawn doesn't\n * hit the same Claude rejection.\n *\n * Returns a short human-readable summary of any action taken (or\n * `null` if no action was warranted), suitable for inclusion in the\n * \"Session unhealthy\" log line.\n */\nexport function prepareForRespawn(codeName: string): string | null {\n const session = sessions.get(codeName);\n if (!session) return null;\n const signature = detectFailureSignature(session.lastFailureTail);\n if (\n signature === 'session_id_in_use' &&\n session.consecutiveSameUuidFailures >= 2\n ) {\n // Capture the count BEFORE resetting so the operator-facing log\n // line carries the actual streak length. The original code reset\n // first and then read 0 (CodeRabbit catch).\n const failureCount = session.consecutiveSameUuidFailures;\n // ENG-5371: rotate at the agent's configured timezone day boundary\n // when one is set; falls back to host-local otherwise.\n const newId = rotateDailySession(\n codeName,\n new Date(),\n session.agentTimezone ?? undefined,\n );\n // Reset counter — fresh UUID, fresh slate.\n session.consecutiveSameUuidFailures = 0;\n session.lastFailureSessionId = null;\n return `rotated daily-session UUID to ${newId} after ${failureCount}+ \"Session ID already in use\" failures`;\n }\n return null;\n}\n\n/**\n * Read the captured pane tail + restart counter for the manager to\n * include in its unhealthy log. Read-only; doesn't mutate session\n * state.\n */\nexport function getLastFailureContext(codeName: string): {\n tail: string | null;\n signature: FailureSignature;\n consecutiveSameUuid: number;\n restartCount: number;\n} {\n const session = sessions.get(codeName);\n return {\n tail: session?.lastFailureTail ?? null,\n signature: detectFailureSignature(session?.lastFailureTail ?? null),\n consecutiveSameUuid: session?.consecutiveSameUuidFailures ?? 0,\n restartCount: session?.restartCount ?? 0,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Session lifecycle (tmux-based)\n// ---------------------------------------------------------------------------\n\nexport function startPersistentSession(config: PersistentSessionConfig): PersistentSession {\n const existing = sessions.get(config.codeName);\n if (existing && existing.status === 'running') {\n return existing;\n }\n\n // Backoff on repeated crashes\n const restartCount = existing?.restartCount ?? 0;\n if (existing?.status === 'crashed' && existing.startedAt) {\n const backoffMs = Math.min(5000 * Math.pow(2, restartCount), 60_000);\n if (Date.now() - existing.startedAt < backoffMs) {\n return existing;\n }\n }\n\n const session: PersistentSession = {\n codeName: config.codeName,\n startedAt: null,\n restartCount,\n status: 'starting',\n currentSessionId: existing?.currentSessionId ?? null,\n lastFailureTail: existing?.lastFailureTail ?? null,\n lastFailureSessionId: existing?.lastFailureSessionId ?? null,\n consecutiveSameUuidFailures: existing?.consecutiveSameUuidFailures ?? 0,\n agentTimezone: config.agentTimezone ?? null,\n };\n sessions.set(config.codeName, session);\n\n spawnSession(config, session);\n return session;\n}\n\nfunction spawnSession(config: PersistentSessionConfig, session: PersistentSession): void {\n const { codeName, projectDir, mcpConfigPath, claudeMdPath, channels, devChannels, apiHost, log } = config;\n const claudeAuthMode = config.claudeAuthMode ?? 'subscription';\n const tmuxSession = `agt-${codeName}`;\n\n log(`[persistent-session] Starting tmux session '${tmuxSession}' for '${codeName}' (auth=${claudeAuthMode})`);\n\n try {\n sanitizeMcpJson(mcpConfigPath, apiHost);\n\n // Also write acpx config for task injection\n writeAcpxConfig(config);\n\n // Kill any existing tmux session (clean slate)\n try {\n execSync(`tmux kill-session -t ${tmuxSession} 2>/dev/null`, { stdio: 'ignore' });\n } catch { /* no existing session */ }\n\n // When running as root, claude looks at $HOME/.claude/.credentials.json\n // Auth mode branch (mutually exclusive — never leave both channels armed):\n //\n // subscription: sync OAuth creds from /home/*/.claude into /root/.claude\n // (idempotent). Do NOT set ANTHROPIC_API_KEY in env.\n // api_key: DELETE any /root/.claude creds so claude can't fall\n // back to a stale OAuth session, then inject\n // ANTHROPIC_API_KEY into the spawn env below.\n //\n // Leaving both present is the \"confused deputy\" path: claude's internal\n // precedence between ANTHROPIC_API_KEY and OAuth has changed between\n // versions and is undocumented. Keep exactly one channel live.\n if (claudeAuthMode === 'subscription') {\n const credsSynced = syncClaudeCredsToRoot();\n if (!credsSynced && platform() === 'linux' && typeof process.getuid === 'function' && process.getuid() === 0) {\n log(`[persistent-session] No Claude Code credentials found under /root/.claude or /home/*. Pair via browser from the host page, or run 'claude /login' on the host.`);\n }\n } else {\n // api_key mode — purge subscription creds under the current user's\n // home. Previously this was hardcoded to /root/.claude, which missed\n // non-root runs and macOS dev setups — letting OAuth creds silently\n // override the api_key in those environments. homedir() is what\n // claude-code itself reads, so that's the directory to clear.\n const claudeDir = join(homedir(), '.claude');\n for (const filename of ['.credentials.json', 'credentials.json']) {\n const p = join(claudeDir, filename);\n if (existsSync(p)) {\n try {\n rmSync(p, { force: true });\n log(`[persistent-session] Removed ${p} (api_key mode active — preventing OAuth fallback)`);\n } catch { /* non-fatal */ }\n }\n }\n if (!config.anthropicApiKey) {\n log(`[persistent-session] api_key mode but no anthropicApiKey passed. Session will fail auth.`);\n }\n }\n\n // Build claude args\n const args: string[] = [];\n\n // ENG-5397: never --resume. Every spawn mints a fresh session UUID\n // and passes it via --session-id, so claude creates a brand-new\n // conversation each time. Continuity moves onto surfaces we own\n // (memory, kanban, channel pending-inbound, audit_log) and is\n // injected into the new session by the SessionStart hook.\n //\n // History: pre-5397 we pinned a UUID per agent per local-day and\n // passed --resume on every same-day respawn. That coupled us to\n // Claude Code's stored-transcript format + Anthropic's request shape\n // — a regression in either (e.g. ENG-5353's 400 role 'system' on\n // 2.1.139) poisoned the transcript and persisted across respawns,\n // taking agents silent for hours (Stirling on 2026-05-22 against a\n // pre-fix session-id, even after the wrapper fix shipped). Fresh\n // every spawn structurally eliminates that failure mode.\n const sessionId = randomUUID();\n args.push('--session-id', sessionId);\n log(`[persistent-session] Starting fresh session ${sessionId} for '${codeName}'`);\n\n // ENG-5431: advance the daily-session marker to today's date with the\n // UUID we just minted. The day-rollover detector in manager-worker.ts\n // reads `current.date` to decide whether to restart this session at\n // the day boundary; if we don't update it on every spawn, that check\n // keeps firing every supervisor tick (the marker stays at the last\n // date written by getOrCreateDailySession / rotateDailySession, which\n // ENG-5397 stopped calling on the spawn path). Writing here keeps the\n // marker in lockstep with the running session, so `isStaleForToday`\n // flips to false on the next tick instead of looping.\n //\n // The write is sync disk IO (atomic tmp+rename inside writeFile()).\n // Isolate it so an ENOSPC / EACCES doesn't take down the spawn flow\n // — the marker is bookkeeping, not on the critical path. Worst case\n // if it fails: next supervisor tick re-fires the day-rollover restart\n // (the bug this fix exists for), but that's strictly no worse than\n // pre-fix behaviour and self-heals the next time the write succeeds.\n try {\n markDailySessionSpawn(codeName, sessionId, new Date(), config.agentTimezone ?? undefined);\n } catch (err) {\n log(\n `[persistent-session] Failed to update daily-session marker for '${codeName}': ${(err as Error).message}`,\n );\n }\n\n if (channels.length > 0) args.push('--channels', ...channels);\n if (devChannels.length > 0) args.push('--dangerously-load-development-channels', ...devChannels);\n args.push('--mcp-config', mcpConfigPath);\n if (existsSync(claudeMdPath)) args.push('--system-prompt-file', claudeMdPath);\n // ENG-5631: pass the agent's model as a session-scoped --model alias.\n // This is the only mechanism that actually takes effect for subscription\n // agents — the model written into the per-agent settings.json isn't read\n // by Claude Code, and a non-interactive agent never runs `/model`. Omit\n // the flag for an empty/unknown model so Claude Code uses the tier default.\n const modelAlias = claudeModelAlias(config.primaryModel);\n if (modelAlias) args.push('--model', modelAlias);\n args.push('--allow-dangerously-skip-permissions');\n args.push('--dangerously-skip-permissions');\n args.push('--strict-mcp-config');\n args.push('--name', tmuxSession);\n\n // Restrict tools to only the agent's configured MCP servers + built-in tools.\n // Without this, agents inherit the user's personal MCPs (Gmail, Calendar, etc.)\n const mcpServerNames = collectMcpServerNames(mcpConfigPath);\n args.push('--allowedTools', buildAllowedTools(mcpServerNames));\n\n // NOTE: CLAUDE_CODE_SIMPLE=1 blocks account plugins BUT also breaks\n // channel auth (Slack/Telegram require claude.ai OAuth). Instead, rely on\n // --strict-mcp-config + --allowedTools for tool isolation. Account plugins\n // may appear in the tool list but --allowedTools prevents calling them.\n //\n // IS_SANDBOX=1 bypasses claude's refusal to run under root/sudo with\n // --dangerously-skip-permissions. Dedicated EC2 hosts running only\n // agent workloads are effectively sandboxed (org-scoped VPC, no inbound,\n // no other tenants). Without this, the tmux session exits immediately\n // with \"cannot be used with root/sudo privileges for security reasons\".\n //\n // ENG-4717: previously we read `.env.integrations` and inlined every\n // KEY=VALUE pair onto the bash command string we handed tmux. That\n // string is the long-running shell process's argv — anything in it is\n // visible via `ps -eo command` for the entire session lifetime, which\n // means tokens like XURL_API_KEY and GRANOLA_ACCESS_TOKEN leaked to\n // any user who could ps on the host. We now write a wrapper script\n // (mode 0700) that sources the env file inside the spawned shell and\n // exec's claude — same pattern as the ACP wrapper below. The argv\n // visible to ps is just `bash <wrapper>`; the secrets never cross the\n // command-line boundary.\n // Every spawn is a fresh session (ENG-5397), so the init prompt is\n // always sent. The SessionStart hook fires before this turn lands\n // and injects the agent's orientation context (today's memory,\n // open kanban, pending channel threads) — by the time the agent\n // responds \"Ready.\" it already knows where it left off.\n const initPrompt =\n 'You are now online. Say \"Ready.\" and wait for incoming messages. Do not run any tools or load any data until a message arrives.';\n const claudeBin = resolveClaudeBinary();\n const claudeArgsJoined = args\n .map(a => (a.includes(' ') || a.includes('*')) ? JSON.stringify(a) : a)\n .join(' ');\n\n const wrapperPath = writePersistentClaudeWrapper({\n projectDir,\n claudeBin,\n initPrompt,\n claudeArgsJoined,\n });\n\n // ANTHROPIC_API_KEY is passed via `tmux new-session -e` so it lands in\n // the session shell's env without ever appearing in the claude shell's\n // argv — `ps aux` on the long-running `bash -c \"claude ...\"` process\n // would otherwise expose the raw key for the session's lifetime.\n // The `-e` flag's exposure is bounded to the new-session invocation,\n // which exits in well under a second.\n const tmuxSessionEnvArgs: string[] = [];\n if (claudeAuthMode === 'api_key' && config.anthropicApiKey) {\n tmuxSessionEnvArgs.push('-e', `ANTHROPIC_API_KEY=${config.anthropicApiKey}`);\n }\n\n // The command tmux runs is just the wrapper path — no secrets, no\n // long token strings, nothing for ps to expose.\n const claudeCmd = JSON.stringify(wrapperPath);\n\n // ENG-4632: defensively backfill HOME/USER before tmux spawns its\n // shell. When the manager is launched via `aws ssm send-command`\n // (or any non-login init), process.env can lack HOME — tmux\n // inherits that, the agent's claude process can't find\n // ~/.claude/.credentials.json, and falls back to the interactive\n // login picker forever. The managerStartCommand also applies this,\n // but a missing HOME at the persistent-session boundary is a\n // belt-and-braces fail-closed point worth keeping.\n const tmuxEnv: NodeJS.ProcessEnv = {\n ...process.env,\n // Treat empty-string as missing too — `HOME=\"\"` makes ~ resolve\n // to cwd, which is the same broken outcome as no HOME, just\n // better hidden.\n HOME: (process.env.HOME?.trim()) || homedir(),\n USER: (process.env.USER?.trim()) || userInfo().username,\n };\n // ENG-5051: stamp AGT_RUN_ID so Claude Code's `${AGT_RUN_ID}`\n // substitution in .mcp.json resolves to a real run for child MCPs\n // (cloud-broker, etc). Without this the persistent session boots\n // with the literal placeholder leaking through and cloud-broker\n // fails its startup guard. Only set when manager-worker successfully\n // minted a run — empty/missing keeps the legacy behaviour\n // (placeholder leaks, agent must mint by hand).\n if (config.runId) {\n tmuxEnv['AGT_RUN_ID'] = config.runId;\n }\n\n // ENG-5901 (CodeRabbit #1731): fail-fast probe — one structured line\n // per `${VAR}` in the rendered .mcp.json that the *actual* spawn env\n // leaves unset or empty. Based on the real tmuxEnv (HOME/USER\n // backfill, AGT_RUN_ID stamp) plus the `-e`-injected\n // ANTHROPIC_API_KEY, overlaid with .env.integrations exactly as the\n // wrapper's `source` will do — not raw process.env, which would\n // false-positive on injected keys. Observational only; the silent\n // failure it catches is \"substitutes to empty → MCP boots → upstream\n // 401s → channel dies\".\n for (const f of probeMcpEnvSubstitution({\n mcpConfigPath,\n envIntegrationsPath: join(projectDir, '.env.integrations'),\n baseEnv: {\n ...tmuxEnv,\n ...(claudeAuthMode === 'api_key' && config.anthropicApiKey\n ? { ANTHROPIC_API_KEY: config.anthropicApiKey }\n : {}),\n },\n })) {\n log(`[persistent-session] ${formatMissingVar(f)} agent=${codeName}`);\n }\n\n // Start tmux session with claude in it\n const child = spawn('tmux', [\n 'new-session', '-d', '-s', tmuxSession, '-c', projectDir,\n ...tmuxSessionEnvArgs, claudeCmd,\n ], {\n cwd: projectDir,\n stdio: ['ignore', 'pipe', 'pipe'],\n env: tmuxEnv,\n });\n\n child.on('close', (code) => {\n if (code !== 0) {\n log(`[persistent-session] Failed to create tmux session for '${codeName}' (exit ${code})`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n return;\n }\n log(`[persistent-session] tmux session '${tmuxSession}' created for '${codeName}'`);\n\n // ENG-4659: redirect pane output to a log file so we can recover\n // claude's actual error message after the session dies (claude's\n // stderr is otherwise unreachable since the tmux child is detached\n // before claude even prints).\n setupPaneLog(tmuxSession, codeName, log);\n\n // Track which session UUID we just spawned with so the recovery\n // hook can detect \"same UUID failing repeatedly\" and rotate.\n // Note: currentSessionId (set on spawn) is distinct from\n // lastFailureSessionId (set on the *previous* failure). Comparing\n // them is what makes the rotation gate work.\n session.currentSessionId = sessionId;\n\n // Auto-accept startup dialogs. Kanban work is driven by the\n // manager-gated hybrid edge-trigger injection (ENG-5435, ENG-5662),\n // not an in-session /loop, so there's nothing to arm here.\n acceptDialogs(tmuxSession, codeName, log, config.primaryModel ?? null, sessionId).catch(() => {});\n });\n\n child.on('error', (err) => {\n log(`[persistent-session] Failed to start tmux for '${codeName}': ${err.message}`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n });\n\n session.startedAt = Date.now();\n session.status = 'running';\n session.restartCount = 0;\n } catch (err) {\n log(`[persistent-session] Failed to start session for '${codeName}': ${(err as Error).message}`);\n session.status = 'crashed';\n session.startedAt = Date.now();\n session.restartCount++;\n }\n}\n\n// ENG-6017: dialog detection + dismissal moved to claude-dialogs.ts so the\n// channel-input-watchdog can share it without an import cycle. The detectors\n// are re-exported through `_internals` below to keep existing tests green.\n\n/**\n * Detect whether the session has actually spawned its MCP server\n * children — the only reliable signal that claude reached the running\n * REPL. tmux pane content alone can't distinguish \"Ready\" prompts\n * from a stuck splash screen, so we shell out to ps and look for\n * children of the claude process.\n *\n * ENG-4634: previously the helper logged 'Session ready' whenever the\n * pane had a `❯` not preceded by 'Enter to confirm' — but the login\n * picker also has a `❯` and would short-circuit out as ready. Verify\n * a real MCP child exists (slack-channel.js / direct-chat-channel.js\n * / etc.) before claiming success.\n */\nfunction hasMcpChildren(tmuxSession: string): boolean {\n try {\n // Find the claude process inside this tmux session by --name flag\n // (set when the manager launches claude — see spawnSession).\n const claudePidOut = execSync(\n `pgrep -f -- \"--name ${tmuxSession}\" 2>/dev/null || true`,\n { encoding: 'utf-8' },\n ).trim();\n if (!claudePidOut) return false;\n // pgrep can match multiple processes (the bash shell wrapping\n // claude, plus claude itself). We want the **claude** process —\n // its children are the MCP servers we're checking for. Process\n // ordering means the wrapper shell is the LOWER PID and claude\n // (forked after the shell parses its args) is HIGHER. Pick the\n // max so `pgrep -P` finds the MCP children, not the bash kids.\n const pids = claudePidOut.split('\\n').map((p) => Number(p)).filter((p) => p > 0);\n if (pids.length === 0) return false;\n const claudePid = Math.max(...pids);\n // List child processes; if any look like an MCP channel server,\n // we're in business.\n const childrenOut = execSync(\n `pgrep -P ${claudePid} 2>/dev/null || true`,\n { encoding: 'utf-8' },\n ).trim();\n if (!childrenOut) return false;\n const childPids = childrenOut.split('\\n').map((p) => p.trim()).filter(Boolean);\n for (const cp of childPids) {\n const cmdline = execSync(\n `cat /proc/${cp}/cmdline 2>/dev/null | tr '\\\\0' ' ' || ps -p ${cp} -o args= 2>/dev/null || true`,\n { encoding: 'utf-8' },\n );\n if (\n /slack-channel\\.js|telegram-channel\\.js|direct-chat-channel\\.js|composio_/i.test(cmdline)\n ) {\n return true;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\nasync function acceptDialogs(\n tmuxSession: string,\n codeName: string,\n log: (msg: string) => void,\n primaryModel: string | null = null,\n sessionId: string | null = null,\n): Promise<void> {\n // Track whether we've already surfaced the login-picker warning so\n // operators don't get one log line per polling iteration. The\n // picker won't dismiss itself — once we've reported it, just keep\n // probing for the eventual recovery (e.g. operator completes OAuth\n // out-of-band) without re-spamming the log.\n let loginPickerReported = false;\n\n // Login-picker iterations don't count against the dialog-dismissal\n // budget — the operator can take minutes to complete OAuth via the\n // Hosts page, and we want acceptDialogs to still be running to\n // dismiss the trust + bypass dialogs that follow. Track the two\n // kinds of iterations separately so a slow OAuth doesn't burn the\n // 30s budget meant for the post-pair dialog cascade. Cap login-\n // picker waits at 15 minutes total to avoid leaking a forever-\n // polling helper if the operator walks away.\n let dialogIterations = 0;\n const MAX_DIALOG_ITERATIONS = 15;\n let loginPickerIterations = 0;\n const MAX_LOGIN_PICKER_ITERATIONS = 450; // 450 * 2s = 15 min\n\n while (\n dialogIterations < MAX_DIALOG_ITERATIONS &&\n loginPickerIterations < MAX_LOGIN_PICKER_ITERATIONS\n ) {\n await new Promise((r) => setTimeout(r, 2000));\n try {\n const screen = execSync(`tmux capture-pane -t ${tmuxSession} -p 2>/dev/null`, { encoding: 'utf-8' });\n\n // ENG-4634: handle the login picker BEFORE any other dialog\n // pattern. The picker has a `❯` cursor that the generic exit\n // branch at the bottom of this loop would otherwise read as\n // \"Session ready\". Press no key — sending Enter would trigger\n // an OAuth flow that requires browser interaction the agent\n // can't complete. Surface a clear, parseable log line so\n // operators / monitoring can route the operator to the\n // Hosts page to complete pairing.\n if (isLoginPickerVisible(screen)) {\n if (!loginPickerReported) {\n log(`[persistent-session] CLAUDE LOGIN REQUIRED for '${codeName}' — agent cannot start until ~/.claude.json is provisioned. Pair via the Hosts page or run 'claude /login' on the host.`);\n loginPickerReported = true;\n }\n loginPickerIterations++;\n continue;\n }\n\n // Reached the dialog cascade — count this iteration against the\n // shorter budget.\n dialogIterations++;\n\n // ENG-6017: the dialog cascade (theme picker → trust → resume-mode →\n // dev channels → MCP confirm → bypass permissions → session feedback)\n // now lives in sweepDialogs() so the channel-input-watchdog and the\n // inject-time hygiene share the exact same recognition. Behaviour and\n // log wording are unchanged (the theme-picker branch still runs before\n // the generic `❯ no Enter to confirm` exit below, since picker rows\n // also render with `❯`).\n const dialogAction = sweepDialogs(screen);\n if (dialogAction) {\n await sendDialogKeys(tmuxSession, dialogAction);\n log(`[persistent-session] ${dialogAction.logMessage} for '${codeName}'`);\n continue;\n }\n if (screen.includes('❯') && !screen.includes('Enter to confirm')) {\n // ENG-4634: don't trust the pane alone. Verify at least one\n // MCP server child has actually been spawned by the claude\n // process before declaring the session ready — otherwise a\n // splash-screen-with-cursor false-positive can race the\n // login picker and leave the agent silently broken.\n if (hasMcpChildren(tmuxSession)) {\n log(`[persistent-session] Session ready for '${codeName}' — MCP servers spawned`);\n // ENG-5770: opt this session into Anthropic's fast-output mode when\n // the agent's primary_model carries the `[fast]` marker. The send\n // happens before any inbound message is injected so the first real\n // prompt of the session is already running in fast mode. Banner is\n // re-checked here to skip + warn if a silent downgrade landed the\n // session on Sonnet/Haiku instead of Opus.\n await maybeSendFastMode({\n tmuxSession,\n codeName,\n primaryModel,\n sessionId,\n screen,\n log,\n });\n break;\n }\n // Pane looks idle but no MCP children yet — claude may still\n // be initialising. Keep polling; the loop bound caps total\n // wait at 30s.\n }\n } catch { break; }\n }\n}\n\n// ---------------------------------------------------------------------------\n// ENG-5770: fast-mode `/fast` send. Called once per ready-banner detection\n// (boot + every respawn path that re-runs acceptDialogs). Skips when the\n// agent isn't on a `[fast]` variant or when the live banner shows a non-Opus\n// family — `/fast` is currently only valid on Opus 4.6/4.7 and a silent\n// model downgrade is the failure mode we want to surface, not paper over.\n// ---------------------------------------------------------------------------\n\ninterface FastModeContext {\n tmuxSession: string;\n codeName: string;\n primaryModel: string | null;\n sessionId: string | null;\n /** Pane capture that triggered the ready-detection; used to inspect the model banner. */\n screen: string;\n log: (msg: string) => void;\n}\n\n/**\n * Inspect the live tmux pane to decide whether `/fast` is safe to send and,\n * if so, deliver it. The decision is intentionally conservative — we only\n * send when the screen mentions `opus`, never when it shows another family,\n * and never when the agent isn't tagged `[fast]`. Failures are logged and\n * swallowed: a missing `/fast` is non-fatal and we don't want a bad capture\n * to crash the spawn path.\n */\nasync function maybeSendFastMode(ctx: FastModeContext): Promise<void> {\n if (!isClaudeFastMode(ctx.primaryModel)) return;\n\n const sid = ctx.sessionId ?? 'unknown';\n const banner = ctx.screen.toLowerCase();\n const hasOpus = banner.includes('opus');\n const hasNonOpus = banner.includes('sonnet') || banner.includes('haiku');\n\n // A pane that mentions a non-Opus family without also mentioning Opus is\n // the silent-downgrade case. Skip + warn so the operator can see why their\n // fast-mode selection didn't take effect.\n if (hasNonOpus && !hasOpus) {\n ctx.log(\n `[fast-mode] skip /fast for agent=${ctx.codeName} session=${sid} — banner shows non-Opus model`,\n );\n return;\n }\n\n // Banner doesn't name a family at all (unusual layout, partial capture).\n // Skip rather than guess; a missing /fast just costs the operator the\n // fast-mode speed-up on this respawn.\n if (!hasOpus) {\n ctx.log(\n `[fast-mode] skip /fast for agent=${ctx.codeName} session=${sid} — Opus not visible in banner`,\n );\n return;\n }\n\n const ok = sendToAgent(ctx.tmuxSession, '/fast');\n if (ok) {\n ctx.log(`[fast-mode] sent /fast for agent=${ctx.codeName} session=${sid}`);\n } else {\n ctx.log(`[fast-mode] failed to send /fast for agent=${ctx.codeName} session=${sid} — tmux send-keys errored`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Prompt-ready detection + tmux send (shared by the manager-gated hybrid\n// kanban-work inject, ENG-5435). The in-session `/loop kanban-work` arm\n// (ENG-5404) that previously lived here was removed in ENG-5662 — kanban work\n// is now driven solely by the hybrid edge-trigger.\n// ---------------------------------------------------------------------------\n\n/**\n * Wait until the Claude Code REPL prompt is ready to accept input.\n *\n * v1 is intentionally simple: poll the pane every 500ms for the `❯ ` prompt\n * marker, return true the first time we see it, return false on timeout.\n * `acceptDialogs()` is the upstream gate that handles dialogs/login pickers,\n * so by the time this runs the pane is either at the prompt or fully wedged.\n *\n * The 10s cap is well past the typical post-acceptDialogs settle time\n * (~1-2s in observation) and short enough that a bad spawn fails fast\n * instead of holding up the manager loop.\n */\nasync function waitForPromptReady(tmuxSession: string): Promise<boolean> {\n const deadline = Date.now() + 10_000;\n while (Date.now() < deadline) {\n try {\n // CodeRabbit PR #1275: execFileSync (not execSync) so tmuxSession\n // is passed as an argv entry rather than interpolated into a\n // shell string. Matches the safer pattern used in acceptDialogs\n // for ENG-5364's resume-mode dismiss path.\n const screen = execFileSync(\n 'tmux',\n ['capture-pane', '-t', tmuxSession, '-p'],\n { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] },\n );\n if (screen.includes('❯ ')) return true;\n } catch { /* session may not exist yet — retry */ }\n await new Promise((r) => setTimeout(r, 500));\n }\n return false;\n}\n\n/**\n * Type of the function used to deliver a command string to the agent's\n * tmux session (via `sendToAgent`). Exposed via the _internals.__setArmSender\n * test seam so unit tests can swap in a spy without touching tmux. The\n * sender returns true on success, false on any tmux/exec failure.\n */\ntype ArmSender = (tmuxSession: string, command: string) => boolean;\n\n// ENG-5793: delay between the text-send and the Enter-send in\n// defaultArmSender. Exposed as a const so future tuning has a single seam.\nexport const SEND_KEYS_ENTER_DELAY_MS = 50;\n\n/**\n * Synchronous millisecond sleep. defaultArmSender is itself synchronous\n * (returns a boolean, not a Promise) and its call sites assume that;\n * Atomics.wait on a transient SharedArrayBuffer is the standard Node\n * idiom for a blocking sleep without changing the function signature.\n * The delay is tiny (50ms) so the cost vs an event-loop sleep is\n * negligible in practice.\n */\nfunction sleepBlockingMs(ms: number): void {\n const view = new Int32Array(new SharedArrayBuffer(4));\n Atomics.wait(view, 0, 0, ms);\n}\n\nfunction defaultArmSender(tmuxSession: string, command: string): boolean {\n try {\n // ENG-5793: split the text and the Enter into two separate\n // `tmux send-keys` invocations.\n //\n // The previous shape was `send-keys -t <s> <text> Enter` in ONE call.\n // When tmux's `extended-keys-format` is `csi-u` (the default on\n // tmux 3.4+ and what AWS EC2 hosts inherit), tmux delivers the\n // whole call as a bracketed paste (the text is multi-byte) and\n // re-encodes the trailing CR (0x0D) inside the paste as a CSI-u\n // sequence (`ESC[13;1u`). Claude Code's bracketed-paste tokenizer\n // doesn't decode CSI-u sequences within paste brackets, so the\n // encoded carriage return is silently dropped. The text lands in\n // the input buffer perfectly readable, the cursor sits at `❯`,\n // the agent never sees a turn. See anthropics/claude-code#43169.\n //\n // Split-call shape:\n // 1. `send-keys -l <text>` — `-l` (literal) sends the text as raw\n // bytes without interpreting key names. tmux still brackets it\n // as a paste, but the paste-end marker fires when this call\n // returns, so the Enter that follows is OUTSIDE the paste.\n // 2. Short sleep — give Claude Code's TUI a tick to commit the\n // paste before the Enter arrives. Without this, the two\n // invocations can be batched into one input window on the\n // app's side, putting us back in the same paste-internal-CR\n // trap.\n // 3. `send-keys Enter` — sent as a standalone keystroke, NOT\n // inside a paste, so the `\\r` is delivered raw and Claude\n // Code treats it as \"submit current input\" as the Kitty\n // protocol spec mandates for the legacy Enter encoding.\n //\n // The old comment here claimed the two-step shape was needed so\n // Claude Code's REPL would parse `/` as a slash command — that\n // turned out to be incidental. Single-call `send-keys text Enter`\n // worked for slash commands only because they're short enough that\n // some tmux/Claude Code versions short-circuited the paste-wrap.\n // The csi-u-vs-paste-tokenizer interaction is the real reason\n // plain-text injects silently fail in the same call shape.\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, '-l', command], {\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n sleepBlockingMs(SEND_KEYS_ENTER_DELAY_MS);\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, 'Enter'], {\n stdio: ['ignore', 'ignore', 'pipe'],\n });\n return true;\n } catch {\n return false;\n }\n}\n\nlet armSender: ArmSender = defaultArmSender;\n\n// ---------------------------------------------------------------------------\n// ENG-6017: inject-time pane hygiene (dialog sweep + orphan-input clear)\n// ---------------------------------------------------------------------------\n\n/**\n * Pane capture used by the pre-send hygiene. Returns null when the session\n * (or tmux itself) is unavailable — callers treat null as \"skip hygiene\",\n * which keeps unit tests and non-tmux environments fast and silent.\n * Swappable via _internals.__setPaneCapture for tests.\n */\ntype PaneCapture = (tmuxSession: string) => string | null;\n\nfunction defaultPaneCapture(tmuxSession: string): string | null {\n try {\n return execFileSync('tmux', ['capture-pane', '-t', tmuxSession, '-p'], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n timeout: 2_000,\n });\n } catch {\n return null;\n }\n}\n\nlet paneCapture: PaneCapture = defaultPaneCapture;\n\n/**\n * Key sender used by the pre-send hygiene (dialog dismissal + C-u clear).\n * One send-keys call per key — never batched, so multi-key sequences can't\n * be wrapped into a single bracketed paste. Swappable via\n * _internals.__setHygieneKeySender for tests.\n */\ntype HygieneKeySender = (\n tmuxSession: string,\n keys: readonly string[],\n interKeyDelayMs: number,\n) => Promise<void>;\n\nconst defaultHygieneKeySender: HygieneKeySender = async (tmuxSession, keys, interKeyDelayMs) => {\n for (let i = 0; i < keys.length; i++) {\n if (i > 0 && interKeyDelayMs > 0) {\n await new Promise((r) => setTimeout(r, interKeyDelayMs));\n }\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, keys[i]!], {\n stdio: 'ignore',\n });\n }\n};\n\nlet hygieneKeySender: HygieneKeySender = defaultHygieneKeySender;\n\n/**\n * ENG-6017: pre-send pane hygiene for the tmux send-keys fallback.\n *\n * Two failure modes observed live (koda, 2026-06-04) that make a blind\n * send-keys land wrong:\n *\n * 1. A dialog (e.g. Claude Code's session-feedback rating prompt)\n * overlays the REPL — the typed text and Enter go into the dialog,\n * not the input box.\n * 2. A previous injection's text is still sitting unsubmitted in the\n * input box — the new text would concatenate onto it, corrupting\n * both messages.\n *\n * So: capture the pane once; dismiss a recognised dialog if present\n * (default-deny — unknown overlays are logged, never keyed); then if the\n * input box still holds orphaned text, clear it with C-u (NEVER submit\n * it — the operator may have already re-sent a corrected version, and\n * blind-submitting a stale ghost instruction is worse than dropping it;\n * the content is logged hash-only per the prod logging policy, and the\n * channel-input-watchdog had its bounded chances to submit it first).\n *\n * Best-effort by design: any capture/send failure skips hygiene and lets\n * the send proceed — this layer must never block delivery. The whole\n * helper is ENG-5927-transitional: it dies with the send-keys fallback\n * when interactive agents move to verified acpx delivery.\n */\nasync function preSendPaneHygiene(\n tmuxSession: string,\n codeName: string,\n log: (msg: string) => void,\n): Promise<void> {\n try {\n let screen = paneCapture(tmuxSession);\n if (screen === null) return;\n\n const action = sweepDialogs(screen);\n if (action) {\n await hygieneKeySender(tmuxSession, action.keys, action.interKeyDelayMs);\n log(`[inject] ${action.logMessage} for '${codeName}' before injection`);\n // Give the TUI a beat to drop the overlay before re-reading the pane.\n await new Promise((r) => setTimeout(r, 300));\n screen = paneCapture(tmuxSession) ?? '';\n }\n\n const orphan = extractInputBoxText(screen);\n if (orphan) {\n log(\n `[inject] clearing orphaned input for '${codeName}' before injection (input_hash=${simpleTextHash(orphan)}, len=${orphan.length})`,\n );\n await hygieneKeySender(tmuxSession, ['C-u'], 0);\n }\n } catch {\n // Hygiene is best-effort — never block the actual send on it.\n }\n}\n\n/**\n * ENG-5435: thin wrapper exposing the tmux send-keys path to the\n * manager-worker (which now also injects prompts via the hybrid\n * kanban-work mode). Routes through the same `armSender` binding so\n * the existing `__setArmSender` test seam still intercepts manager-side\n * calls. Returns true on success, false on any tmux/exec failure.\n */\nexport function sendToAgent(tmuxSession: string, command: string): boolean {\n return armSender(tmuxSession, command);\n}\n\n/**\n * ENG-5435: exported so the manager-worker hybrid path can gate\n * injects on a ready prompt. Same 10s cap and `❯` detection as the\n * spawn-time arm uses. Kept on `_internals` too for existing tests.\n */\nexport async function isAgentPromptReady(tmuxSession: string): Promise<boolean> {\n return waitForPromptReady(tmuxSession);\n}\n\n// Exported for unit testing — see __tests__/persistent-session-dialogs.test.ts.\nexport const _internals = {\n isLoginPickerVisible,\n isResumeModeDialogVisible,\n detectFailureSignature,\n isClaudeProcessAliveInTmux,\n waitForPromptReady,\n // ENG-5770: exported so the unit test in claude-model-alias.test.ts can\n // exercise the send/skip decision without spawning a real tmux session.\n maybeSendFastMode,\n // Test seam: swap the tmux send-keys path so tests don't have to\n // spawn a real tmux server. ESM module-binding makes\n // `vi.spyOn(module, 'execFileSync')` ineffective for in-module\n // callers, so we route through this shim.\n __setArmSender(fn: ArmSender | null): void {\n armSender = fn ?? defaultArmSender;\n },\n // ENG-6017 test seam: swap the pane capture used by the inject-time\n // hygiene so unit tests can simulate dialog overlays / orphaned input\n // without a tmux server. null restores the real capture.\n __setPaneCapture(fn: PaneCapture | null): void {\n paneCapture = fn ?? defaultPaneCapture;\n },\n // ENG-6017 test seam: swap the hygiene key sender (dialog dismissal +\n // C-u clear) so unit tests can assert keystrokes without tmux.\n __setHygieneKeySender(fn: HygieneKeySender | null): void {\n hygieneKeySender = fn ?? defaultHygieneKeySender;\n },\n // ENG-5758 test seam: swap the acpx session probe so unit tests can\n // simulate \"session present\" / \"no session\" without spawning acpx.\n __setAcpxSessionProbe(fn: AcpxSessionProbe | null): void {\n acpxSessionProbe = fn ?? defaultAcpxSessionProbe;\n acpxSessionAvailableCache.clear();\n },\n // ENG-5758 test seam: pin the resolved acpx binary path so unit tests can\n // exercise the \"acpx present but no session\" branch deterministically,\n // independent of what's installed in the test node_modules. null re-resolves.\n __setAcpxBin(path: string | null): void {\n _acpxBin = path;\n },\n // Test-only resets so each test starts from a clean slate.\n __resetZombieState(): void {\n zombieProbeCache.clear();\n pendingZombieDetections.clear();\n },\n __resetAcpxSessionCache(): void {\n acpxSessionAvailableCache.clear();\n },\n __getSessionsMap(): Map<string, PersistentSession> {\n return sessions;\n },\n __peekPendingZombie(codeName: string): ZombieDetectionRecord | null {\n return pendingZombieDetections.get(codeName) ?? null;\n },\n};\n\n// ---------------------------------------------------------------------------\n// Task injection (acpx preferred, tmux send-keys fallback)\n// ---------------------------------------------------------------------------\n\n// ENG-5599: richer inject outcome so callers can tell \"confirmed delivery\"\n// (acpx exec) apart from \"sent but unverified\" (tmux send-keys fallback) apart\n// from \"not delivered at all\". The scheduled-task route needs this: a bare\n// `false` (which send-keys returns even after sending the keys) would make it\n// both nudge the live session AND spawn the legacy claude -p oneshot — double-\n// executing the task. `delivered` = confirmed; `fallbackUsed` = send-keys ran\n// (the keystroke landed, submission unverified); neither = genuine failure.\nexport interface InjectResult {\n delivered: boolean;\n fallbackUsed: boolean;\n}\n\n// Boolean-returning wrapper preserved for the many existing callers. `true`\n// still means \"confirmed delivery via acpx\" exactly as before (send-keys and\n// failure both collapse to false), so behaviour is unchanged for them.\nexport async function injectMessage(\n codeName: string,\n type: 'task' | 'chat' | 'system',\n content: string,\n meta?: Record<string, string>,\n log?: (msg: string) => void,\n): Promise<boolean> {\n return (await injectMessageWithStatus(codeName, type, content, meta, log)).delivered;\n}\n\nexport async function injectMessageWithStatus(\n codeName: string,\n type: 'task' | 'chat' | 'system',\n content: string,\n meta?: Record<string, string>,\n log?: (msg: string) => void,\n): Promise<InjectResult> {\n const _log = log ?? ((_: string) => {});\n const session = sessions.get(codeName);\n if (!session || session.status !== 'running') {\n _log(`[inject] SKIP '${codeName}' — session ${session ? `status=${session.status}` : 'not found in Map'}`);\n return { delivered: false, fallbackUsed: false };\n }\n\n const prefix = meta?.task_name ? `[Task: ${meta.task_name}] ` : '';\n const text = prefix + content;\n const projectDir = getProjectDir(codeName);\n\n // Preferred: use acpx exec for reliable injection (no paste issues) — but\n // ONLY when an acpx session actually exists for this agent. Without the\n // session gate, exec exits 4 (\"No acpx session found\") and, because the\n // spawn is detached/stdio:'ignore'/fire-and-forget, the failure is silent\n // and we'd report delivered:true while dropping the message (ENG-5758).\n const acpx = getAcpxBin();\n if (acpx && hasAcpxSession(acpx, projectDir, codeName)) {\n try {\n // Write prompt to temp file to avoid shell escaping issues\n const tmpDir = join(projectDir, '.claude');\n mkdirSync(tmpDir, { recursive: true });\n const tmpFile = join(tmpDir, '.agt-inject-prompt.txt');\n writeFileSync(tmpFile, text);\n\n _log(`[inject] acpx exec (fire-and-forget): cwd=${projectDir}, file=${tmpFile}`);\n const child = spawn(acpx, ['claude', 'exec', '-f', tmpFile], {\n cwd: projectDir,\n stdio: 'ignore',\n detached: true,\n });\n child.on('error', (err) => {\n _log(`[inject] acpx spawn error for '${codeName}': ${err.message}`);\n });\n // ENG-5758: surface non-zero exits — previously swallowed, which hid\n // the no-session failure. Invalidate the cache so the next inject\n // re-probes (the session may have gone away mid-flight).\n child.on('exit', (code) => {\n if (code && code !== 0) {\n _log(`[inject] acpx exec exited ${code} for '${codeName}' — message may not have been delivered`);\n acpxSessionAvailableCache.delete(codeName);\n }\n });\n child.unref();\n return { delivered: true, fallbackUsed: false };\n } catch (err) {\n _log(`[inject] acpx exec failed for '${codeName}': ${(err as Error).message}`);\n // Fall through to tmux\n }\n } else if (acpx) {\n _log(`[inject] no acpx session for '${codeName}' — falling back to tmux send-keys`);\n } else {\n _log(`[inject] acpx binary not found — falling back to tmux send-keys`);\n }\n\n // Fallback: tmux send-keys via the shared armSender seam (testable, and\n // submits with a trailing Enter — see defaultArmSender).\n //\n // ENG-5782: collapse newlines before handing to tmux. Multi-line content\n // triggers tmux's bracketed-paste wrapping when passed to send-keys; once\n // Claude Code's TUI sees the paste-start escape, the trailing `Enter`\n // argument is captured as a literal newline *within* the paste rather\n // than as a \"submit current input\" event. The result: the prompt lands\n // in the input buffer perfectly readable but is never sent. The acpx\n // path above doesn't have this problem (file-loaded user turn, no\n // keystroke layer), so callers can keep building multi-line content\n // (run-boundary markers on their own line, structured nudges, etc.)\n // and we adapt it here for the fallback. Inert markers like\n // <!-- agt-run:UUID --> remain parseable on a single line — see the\n // RUN_MARKER_RE regex which doesn't care about line position.\n const singleLineText = text.replace(/\\s*\\n+\\s*/g, ' ').trim();\n // ENG-6017: dismiss any dialog overlaying the REPL and clear orphaned\n // input-box text before typing, so the send can't be eaten by a dialog\n // or concatenate onto a previously-stuck message.\n await preSendPaneHygiene(`agt-${codeName}`, codeName, _log);\n const sent = sendToAgent(`agt-${codeName}`, singleLineText);\n if (sent) {\n // tmux send-keys doesn't guarantee submission, so it's not a *confirmed*\n // delivery (delivered:false) — but the keystroke did land, so fallbackUsed\n // is true. injectMessage() still collapses this to `false` for legacy\n // callers; callers that must not double-act (the scheduled-task route)\n // read fallbackUsed and treat it as \"reached the session, don't also spawn\n // the oneshot\".\n _log(`[inject] tmux send-keys sent for '${codeName}' — unverified (delivered=false, fallbackUsed=true)`);\n return { delivered: false, fallbackUsed: true };\n }\n _log(`[inject] tmux send-keys failed for '${codeName}'`);\n return { delivered: false, fallbackUsed: false };\n}\n\n// ---------------------------------------------------------------------------\n// Session management\n// ---------------------------------------------------------------------------\n\nexport function stopPersistentSession(codeName: string, log: (msg: string) => void): void {\n const session = sessions.get(codeName);\n if (!session) return;\n\n log(`[persistent-session] Stopping session for '${codeName}'`);\n session.status = 'stopped';\n\n try {\n execSync(`tmux kill-session -t agt-${codeName} 2>/dev/null`, { stdio: 'ignore' });\n } catch { /* session may already be dead */ }\n\n // Also close any acpx session\n try {\n const acpx = getAcpxBin();\n if (acpx) {\n execFileSync(acpx, ['claude', 'sessions', 'close', `agt-${codeName}`], {\n cwd: getProjectDir(codeName),\n timeout: 5_000,\n stdio: 'ignore',\n });\n }\n } catch { /* non-fatal */ }\n\n sessions.delete(codeName);\n // ENG-5758: drop the cached acpx-session probe so a respawned session is\n // re-probed rather than inheriting the dead session's verdict.\n acpxSessionAvailableCache.delete(codeName);\n\n // ENG-4808: claude exiting should take its child channel-MCP processes\n // (telegram-channel.js, slack-channel.js, direct-chat-channel.js) with\n // it, but in practice those children survive the parent — node's stdio\n // close-on-parent-exit isn't always honoured, especially when claude is\n // killed via tmux SIGHUP. Without an explicit reap, every restart leaks\n // a tree of long-pollers each holding the agent's bot token (observed\n // 6+ orphans on agt-aws-1 during the Vigil debugging session). Schedule\n // the reap after a short delay so claude has a chance to clean up its\n // own children — anything still alive after that is fair game.\n setTimeout(() => {\n reapOrphanChannelMcps({ log });\n }, 3_000).unref();\n}\n\nexport function getSessionState(codeName: string): PersistentSession | null {\n return sessions.get(codeName) ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Zombie detection (ENG-5391)\n//\n// Claude can die inside a live tmux pane — the shell prompt is left behind,\n// `tmux has-session` keeps reporting the session as alive, and the manager's\n// existing health check never trips. On alyve-host-1 (2026-05-21) this caused\n// ~3-7 hours of silent unresponsiveness on `dwight` before the tmux session\n// itself was eventually replaced.\n//\n// We close the gap by probing for the actual claude process inside each\n// \"healthy\" tmux session. The probe matches on `--name <tmuxSession>` —\n// the same flag the manager passes to claude at spawn (see line ~607),\n// reused successfully by `hasMcpChildren()`. When the probe says no claude\n// process is alive, we treat the session as crashed, kill the dead tmux\n// shell so the next spawn isn't blocked by \"duplicate session\", and stash\n// a record for manager-worker to ship as an audit_log event.\n//\n// Cost guard: `isSessionHealthy` is called many times per tick per agent.\n// A short TTL cache (ZOMBIE_PROBE_TTL_MS) keeps the pgrep call at one per\n// ~30s per agent — well inside the < 5 min detection target.\n//\n// Grace window: claude can take 10-30s to fully start. We don't probe\n// within the first ZOMBIE_STARTUP_GRACE_MS after spawn / re-discovery,\n// which avoids false positives during boot.\n// ---------------------------------------------------------------------------\n\nconst ZOMBIE_PROBE_TTL_MS = 30_000;\nconst ZOMBIE_STARTUP_GRACE_MS = 60_000;\n\ninterface ZombieProbeCacheEntry {\n at: number;\n alive: boolean;\n}\nconst zombieProbeCache = new Map<string, ZombieProbeCacheEntry>();\n\nexport interface ZombieDetectionRecord {\n codeName: string;\n tmuxSession: string;\n detectedAt: number;\n /** Last few lines of the pane log captured at detection time. */\n paneTail: string | null;\n}\nconst pendingZombieDetections = new Map<string, ZombieDetectionRecord>();\n\n// ENG-5832: the pgrep matching (ERE anchoring + the `--` option-terminator\n// guard) moved to @augmented/core `runtime/session-probe.ts` so the channel\n// servers reuse the exact same logic. The shared primitive is tri-state\n// (alive | dead | unknown) to stay safe to reuse on hosts without pgrep; the\n// manager only ever cared about the boolean \"is claude alive\", and treats\n// both 'dead' and 'unknown' as crashed — exactly the pre-extraction behaviour\n// (the old local copy returned false on any catch, ENOENT included).\nfunction isClaudeProcessAliveInTmux(tmuxSession: string): boolean {\n return probeClaudeProcessInTmux(tmuxSession) === 'alive';\n}\n\n/**\n * Pop the pending zombie-detection record for an agent, if any. Manager-\n * worker calls this in the unhealthy-session branch to emit an audit_log\n * event before respawning. Idempotent — returns null once consumed.\n */\nexport function takeZombieDetection(codeName: string): ZombieDetectionRecord | null {\n const record = pendingZombieDetections.get(codeName);\n if (record) pendingZombieDetections.delete(codeName);\n return record ?? null;\n}\n\n/**\n * Check if a persistent session is healthy.\n *\n * Two conditions both need to hold:\n * 1. The tmux session named `agt-<codeName>` exists (existing check).\n * 2. A claude process is actually running inside it (ENG-5391).\n *\n * Also detects sessions from previous manager runs (not in the Map).\n */\nexport function isSessionHealthy(codeName: string): boolean {\n const tmuxSession = `agt-${codeName}`;\n\n // Check if tmux session exists\n try {\n execSync(`tmux has-session -t ${tmuxSession} 2>/dev/null`, { stdio: 'ignore' });\n } catch {\n // tmux session doesn't exist — mark as crashed but don't increment\n // restartCount here (that happens in spawnSession on actual failure)\n const session = sessions.get(codeName);\n if (session && session.status === 'running') {\n session.status = 'crashed';\n // ENG-4659: capture the pane log tail BEFORE the next spawn\n // overwrites pane.log. Stash it on the session so the next\n // unhealthy log line + the prepareForRespawn recovery hook can\n // both read it. Also track consecutive failures with the same\n // UUID so we only rotate after >= 2 fails (one transient failure\n // doesn't lose today's history).\n session.lastFailureTail = readPaneLogTail(codeName);\n // The UUID just used to spawn (currentSessionId) vs the UUID that\n // last failed (lastFailureSessionId). When they match, we're\n // failing on the same UUID twice in a row — increment the gate.\n // When they differ (fresh spawn after rotation, first-ever\n // failure, etc.) reset to 1. Comparing lastFailureSessionId to\n // itself was the original CodeRabbit-caught bug.\n const failedUuid = session.currentSessionId;\n if (failedUuid && failedUuid === session.lastFailureSessionId) {\n session.consecutiveSameUuidFailures += 1;\n } else {\n session.consecutiveSameUuidFailures = 1;\n }\n session.lastFailureSessionId = failedUuid;\n }\n return false;\n }\n\n // tmux session exists — ensure it's tracked in the Map\n if (!sessions.has(codeName)) {\n sessions.set(codeName, {\n codeName,\n startedAt: Date.now(),\n restartCount: 0,\n status: 'running',\n currentSessionId: null,\n lastFailureTail: null,\n lastFailureSessionId: null,\n consecutiveSameUuidFailures: 0,\n agentTimezone: null,\n });\n }\n\n const session = sessions.get(codeName)!;\n if (session.status !== 'running') {\n session.status = 'running';\n }\n\n // ENG-5391: tmux session exists — verify a claude process is actually\n // running inside it. Skip during the startup grace window so we don't\n // false-positive on a session that's still booting.\n const startedAt = session.startedAt;\n const withinGrace =\n startedAt != null && (Date.now() - startedAt) < ZOMBIE_STARTUP_GRACE_MS;\n\n if (!withinGrace) {\n const cached = zombieProbeCache.get(codeName);\n const cacheFresh = cached !== undefined && (Date.now() - cached.at) < ZOMBIE_PROBE_TTL_MS;\n const claudeAlive = cacheFresh ? cached.alive : isClaudeProcessAliveInTmux(tmuxSession);\n if (!cacheFresh) {\n zombieProbeCache.set(codeName, { at: Date.now(), alive: claudeAlive });\n }\n\n if (!claudeAlive) {\n // Zombie state: tmux session lingers but claude is gone. Capture\n // the pane tail BEFORE we kill the session so the next \"unhealthy\"\n // log line has the same forensics the regular crash path gets.\n const paneTail = readPaneLogTail(codeName);\n\n // Tear down the dead tmux session so the next spawnSession's\n // `tmux new-session` doesn't fail with \"duplicate session\" and\n // wedge the agent in a permanent zombie.\n try {\n execFileSync('tmux', ['kill-session', '-t', tmuxSession], { stdio: 'ignore' });\n } catch {\n // Race: another caller (or `stopPersistentSession`) already killed\n // it. The end state we want is unchanged either way.\n }\n\n session.status = 'crashed';\n session.lastFailureTail = paneTail;\n // Same consecutive-failure bookkeeping the tmux-gone path does so\n // the rotation gate behaves consistently across both crash modes.\n const failedUuid = session.currentSessionId;\n if (failedUuid && failedUuid === session.lastFailureSessionId) {\n session.consecutiveSameUuidFailures += 1;\n } else {\n session.consecutiveSameUuidFailures = 1;\n }\n session.lastFailureSessionId = failedUuid;\n\n // Stash for manager-worker to forward as an audit_log event. Only\n // record on transition (don't overwrite an unconsumed record) so a\n // hot loop of unhealthy checks doesn't emit duplicate audit rows.\n if (!pendingZombieDetections.has(codeName)) {\n pendingZombieDetections.set(codeName, {\n codeName,\n tmuxSession,\n detectedAt: Date.now(),\n // Cap pane tail to keep the audit payload bounded.\n paneTail: paneTail ? paneTail.slice(-1000) : null,\n });\n }\n zombieProbeCache.delete(codeName);\n return false;\n }\n }\n\n return true;\n}\n\nexport function resetRestartCount(codeName: string): void {\n const session = sessions.get(codeName);\n if (session) session.restartCount = 0;\n}\n\n// ---------------------------------------------------------------------------\n// Diagnostics — collect session health info for remote debugging\n// ---------------------------------------------------------------------------\n\nexport interface SessionDiagnostics {\n codeName: string;\n status: 'running' | 'starting' | 'stopped' | 'crashed' | 'unknown';\n startedAt: string | null;\n restartCount: number;\n tmuxAlive: boolean;\n screenCapture: string | null; // last N lines from tmux pane\n launchArgs: string | null; // process args\n channelStatus: string | null; // extracted from screen capture\n}\n\nexport function collectDiagnostics(codeNames: string[]): SessionDiagnostics[] {\n return codeNames.map((codeName) => {\n const session = sessions.get(codeName);\n const tmuxSession = `agt-${codeName}`;\n let tmuxAlive = false;\n let screenCapture: string | null = null;\n let launchArgs: string | null = null;\n let channelStatus: string | null = null;\n\n // Check tmux session (execFileSync to avoid shell injection)\n try {\n execFileSync('tmux', ['has-session', '-t', tmuxSession], { stdio: 'ignore' });\n tmuxAlive = true;\n } catch { /* session doesn't exist */ }\n\n // Capture last 30 lines from tmux pane\n if (tmuxAlive) {\n try {\n screenCapture = execFileSync('tmux', ['capture-pane', '-t', tmuxSession, '-p', '-S', '-30'], {\n encoding: 'utf-8',\n timeout: 3000,\n }).trim();\n } catch { /* non-fatal */ }\n }\n\n // Get process args via ps (safe — no user input in command)\n try {\n const psOutput = execFileSync('ps', ['aux'], { encoding: 'utf-8', timeout: 3000 });\n const line = psOutput.split('\\n').find((l) => l.includes(`agt-${codeName}`) && !l.includes('grep'));\n if (line) {\n const match = line.match(/claude\\s+.*/);\n launchArgs = match ? match[0].slice(0, 500) : null;\n }\n } catch { /* non-fatal */ }\n\n // Extract channel status from screen capture.\n // Only check the last 5 lines for current state — startup errors\n // may linger in scroll history but the agent could be healthy now.\n if (screenCapture) {\n const recentLines = screenCapture.split('\\n').slice(-5).join('\\n');\n const isIdle = recentLines.includes('❯');\n\n if (isIdle) {\n // Agent is at prompt — channels are likely working\n // Check full capture for persistent errors only\n if (screenCapture.includes('Channels require claude.ai authentication')) {\n channelStatus = 'error: auth required';\n } else {\n channelStatus = 'ok';\n }\n } else if (recentLines.includes('CHANNEL_ERROR') || recentLines.includes('CLOSED')) {\n channelStatus = 'error: disconnected';\n } else if (recentLines.includes('no MCP server configured')) {\n channelStatus = 'error: MCP server not found';\n } else if (recentLines.includes('ignored')) {\n channelStatus = 'error: channels ignored';\n } else {\n channelStatus = 'ok';\n }\n }\n\n return {\n codeName,\n status: tmuxAlive\n ? (session?.status ?? 'running')\n : (session?.status === 'running' ? 'crashed' : session?.status ?? 'unknown'),\n startedAt: session?.startedAt ? new Date(session.startedAt).toISOString() : null,\n restartCount: session?.restartCount ?? 0,\n tmuxAlive,\n screenCapture: screenCapture ? screenCapture.slice(-2000) : null, // limit size\n launchArgs,\n channelStatus,\n };\n });\n}\n\nexport function stopAllSessions(log: (msg: string) => void): void {\n for (const codeName of sessions.keys()) {\n stopPersistentSession(codeName, log);\n }\n}\n\nexport async function stopAllSessionsAndWait(\n log: (msg: string) => void,\n opts: { timeoutMs: number },\n): Promise<void> {\n const codeNames = [...sessions.keys()];\n if (codeNames.length === 0) return;\n\n for (const codeName of codeNames) {\n stopPersistentSession(codeName, log);\n }\n\n await new Promise<void>((resolve) => setTimeout(resolve, Math.min(opts.timeoutMs, 2000)));\n}\n\nexport function getProjectDir(codeName: string): string {\n return join(homedir(), '.augmented', codeName, 'project');\n}\n\n// ---------------------------------------------------------------------------\n// acpx config (needed for prompt-based injection)\n// ---------------------------------------------------------------------------\n\nfunction writeAcpxConfig(config: PersistentSessionConfig): void {\n const {\n projectDir,\n mcpConfigPath,\n claudeMdPath,\n channels,\n devChannels,\n anthropicApiKey,\n } = config;\n const claudeAuthMode = config.claudeAuthMode ?? 'subscription';\n\n const claudeArgs: string[] = [];\n if (channels.length > 0) claudeArgs.push('--channels', ...channels);\n if (devChannels.length > 0) claudeArgs.push('--dangerously-load-development-channels', ...devChannels);\n claudeArgs.push('--mcp-config', mcpConfigPath);\n if (existsSync(claudeMdPath)) claudeArgs.push('--system-prompt-file', claudeMdPath);\n // ENG-5631: keep ACP-injected turns on the same model as the tmux session.\n const acpModelAlias = claudeModelAlias(config.primaryModel);\n if (acpModelAlias) claudeArgs.push('--model', acpModelAlias);\n claudeArgs.push('--allow-dangerously-skip-permissions');\n claudeArgs.push('--dangerously-skip-permissions');\n claudeArgs.push('--strict-mcp-config');\n\n // Tool isolation for acpx exec (same as tmux session)\n const mcpServerNames2 = collectMcpServerNames(mcpConfigPath);\n claudeArgs.push('--allowedTools', buildAllowedTools(mcpServerNames2));\n\n // Write a wrapper script that sources .env.integrations then runs the ACP\n // adapter. This avoids ENAMETOOLONG from inlining long tokens (e.g. Xero\n // JWTs) into the command string, and works around acpx not supporting an\n // `env` field on agent configs.\n const acpCmd = `npx -y @agentclientprotocol/claude-agent-acp ${claudeArgs.map(a => (a.includes(' ') || a.includes('*')) ? JSON.stringify(a) : a).join(' ')}`;\n const envIntegrationsPath = join(projectDir, '.env.integrations');\n const wrapperPath = join(projectDir, '.claude', 'acpx-agent.sh');\n const wrapperLines = ['#!/usr/bin/env bash'];\n if (existsSync(envIntegrationsPath)) {\n wrapperLines.push(`set -a`, `source ${JSON.stringify(envIntegrationsPath)}`, `set +a`);\n }\n // Mirror the tmux-session auth branch: when mode=api_key we've purged the\n // OAuth creds under /root/.claude, so ACP task injections (acpx) also need\n // ANTHROPIC_API_KEY or every injected task fails auth. JSON.stringify is\n // shell-safe under bash for sk-ant-* tokens (no $/` chars).\n if (claudeAuthMode === 'api_key' && anthropicApiKey) {\n wrapperLines.push(`export ANTHROPIC_API_KEY=${JSON.stringify(anthropicApiKey)}`);\n }\n wrapperLines.push(`exec ${acpCmd}`);\n mkdirSync(join(projectDir, '.claude'), { recursive: true });\n // ENG-4717: writeFileSync's mode option only applies on file\n // creation; chmodSync afterwards enforces 0755 on overwrites too.\n writeFileSync(wrapperPath, wrapperLines.join('\\n') + '\\n', { mode: 0o755 });\n chmodSync(wrapperPath, 0o755);\n\n const acpxConfig = {\n defaultAgent: 'claude',\n defaultPermissions: 'approve-all',\n agents: {\n claude: {\n command: wrapperPath,\n },\n },\n };\n\n writeFileSync(join(projectDir, '.acpxrc.json'), JSON.stringify(acpxConfig, null, 2));\n}\n","/**\n * Sanitize a Claude Code .mcp.json file for compatibility.\n *\n * Fixes:\n * 1. Relative proxy URLs (e.g., /mcp-proxy/...) — resolved to absolute if\n * apiHost is provided, otherwise removed.\n * 2. URL-based entries (type: \"sse\") — converted to mcp-remote stdio bridge\n * since Claude Code doesn't support SSE MCP servers natively.\n *\n * Returns true if the file was modified.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\n\nexport function sanitizeMcpJson(\n mcpConfigPath: string,\n apiHost?: string,\n): boolean {\n try {\n const mcpRaw = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));\n const servers = mcpRaw.mcpServers as Record<string, Record<string, unknown>> | undefined;\n if (!servers) return false;\n\n let changed = false;\n for (const [key, val] of Object.entries(servers)) {\n if (typeof val?.url !== 'string') continue;\n\n // Resolve relative URLs\n if (val.url.startsWith('/')) {\n if (apiHost) {\n val.url = `${apiHost}${val.url}`;\n changed = true;\n } else {\n delete servers[key];\n changed = true;\n continue;\n }\n }\n\n // ENG-5071: do NOT wrap URL-based entries that carry auth headers in\n // mcp-remote. mcp-remote can't pass headers through to the upstream\n // MCP server, so the conversion silently drops the Authorization\n // header and the call fails at runtime. claudecode/index.ts's\n // writeMcpServer explicitly emits the raw `{ url, headers }` shape\n // for this case (ENG-4694).\n //\n // ENG-5074: Claude Code's MCP schema additionally requires a `type`\n // field on URL-based entries — without it claude rejects the\n // config at startup (\"Does not adhere to MCP server configuration\n // schema\") and the tmux session exits inside a second, putting\n // the agent in a respawn loop. Existing on-disk files written by\n // the pre-ENG-5074 writer carry url+headers but no type — backfill\n // 'http' (Streamable HTTP, the default for OAuth-MCP integrations)\n // so the sanitizer self-heals these entries instead of leaving\n // them to fail in claude. New writes from the post-ENG-5074\n // writer already include the field, so this is a no-op for them.\n const headers = val.headers as Record<string, unknown> | undefined;\n if (headers && typeof headers === 'object' && Object.keys(headers).length > 0) {\n if (typeof val.type !== 'string') {\n val.type = 'http';\n changed = true;\n }\n continue;\n }\n\n // Convert URL-based entries to mcp-remote stdio bridge\n // Claude Code doesn't support type: \"sse\" natively\n const url = val.url as string;\n delete val.url;\n delete val.type;\n val.command = 'npx';\n val.args = ['-y', 'mcp-remote', url, '--allow-http'];\n changed = true;\n }\n\n if (changed) writeFileSync(mcpConfigPath, JSON.stringify(mcpRaw, null, 2));\n return changed;\n } catch {\n return false;\n }\n}\n","// Shared helper for building Claude Code's --allowedTools string (ENG-4487).\n//\n// The manager spawns claude in three modes: persistent tmux session, acpx\n// exec wrapper, and one-shot `claude -p` for scheduled tasks + webapp direct\n// chat. Each site used to hand-roll its own allowedTools list, which drifted:\n// the one-shot paths forgot Skill and Agent, so integration skills under\n// .claude/skills/integration-... were silently invisible during scheduled-task\n// execution. Agents produced apologetic \"no data sources connected\" outputs\n// when the skills were actually on disk and their API keys were in env\n// vars — they just couldn't call the Skill tool.\n//\n// Invariant: every Claude Code invocation the manager spawns must include\n// Skill, Agent, and ToolSearch. Their absence disables integration-skill\n// activation, subagent dispatch, and MCP tool binding respectively, all\n// without warning. Keep that list in one place so a new spawn site\n// physically cannot miss them.\n//\n// ENG-5926: ToolSearch added. Modern Claude Code surfaces MCP tools via\n// the deferred-tool registry — tools start as schemas in ToolSearch's\n// catalog and bind on first invocation. Without ToolSearch in\n// `--allowedTools`, the entire MCP toolchain is invisible to a\n// dispatched sub-agent. Don's empirical evidence 2026-06-03 on\n// agt-aws-1: every `mcp__*` call from a Task-dispatched sub-agent\n// returned \"No such tool available.\" despite the wildcards being in\n// the sub-agent's `tools:` allowlist line. ToolSearch was the\n// missing piece — parent uses it for first-tool-call binding,\n// sub-agents inherit nothing if they don't have it. The parent\n// session also benefits (its own MCP first-call binding goes through\n// the same path). Adding it on every spawn closes the entire ENG-5897\n// → ENG-5905 → ENG-5922 → ENG-5924 → ENG-5926 thread.\n\n// Order is stable for test snapshots.\nconst BASE_TOOLS = ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Agent', 'Skill', 'ToolSearch'] as const;\n\n// Build the comma-separated allowedTools string for a Claude Code spawn.\n// Each MCP server name becomes a wildcard pattern matching every tool that\n// server exposes; plus the nine base built-ins (incl. ToolSearch for MCP\n// lazy-load — see ENG-5926).\nexport function buildAllowedTools(mcpServerNames: readonly string[]): string {\n // Claude Code's allowedTools patterns use underscore-separated names; MCP\n // server IDs in .mcp.json can use hyphens (e.g. direct-chat), so normalise.\n const mcpPatterns = mcpServerNames.map((name) => `mcp__${name.replace(/-/g, '_')}__*`);\n return [...mcpPatterns, ...BASE_TOOLS].join(',');\n}\n","import { existsSync, readFileSync } from 'node:fs';\n\n/**\n * ENG-5901 (ADR-0018 Phase 1) — fail-fast probe for `${VAR}` substitution\n * gaps at MCP spawn time.\n *\n * `.mcp.json` carries `${VAR}` placeholders that Claude Code substitutes\n * from the spawn environment (ADR-0006). Two failure modes:\n *\n * - var **unset** with no `:-default`: Claude Code refuses to parse the\n * config at startup (loud, per the Claude Code MCP docs). Still worth\n * a structured line so the operator's first grep explains claude's\n * parse error.\n * - var **set but empty** (`FOO=` line in `.env.integrations`, or an\n * empty export): substitution \"succeeds\" with `\"\"`, the MCP boots,\n * the upstream API 401s, and the channel dies silently. This is the\n * case the probe exists for.\n *\n * The probe is **observational only** — it emits one structured stderr\n * line per gap and never blocks the spawn. Greppable signature\n * (documented in docs/operator/credential-migration-eng5898.md):\n *\n * [mcp-env-substitution] missing var=<NAME> server=<KEY> state=<unset|empty>\n *\n * Pure helpers + a thin fs wrapper; unit-testable without a spawn.\n */\n\n/**\n * Vars that are legitimately absent/empty at probe time because a later\n * layer binds them (or deliberately leaves them unset):\n *\n * - AGT_RUN_ID: exported per-spawn by the manager for scheduled runs\n * (ENG-4561) and intentionally unset for sessions with no `runs` row\n * (the augmented bridge maps missing → null run id; ENG-5818).\n * - AGT_TOKEN: exchanged at runtime by the broker (missing makes it\n * fall back to AGT_API_KEY → /host/exchange); never in the spawn env\n * by design. Flagged as noise on agt-aws-1 (stirling/xero).\n * - ANCHOR_BROWSER_SESSION_ID: seeded EMPTY into .env.integrations by\n * remoteMcp envDefaults (ENG-5855) and minted per-session later\n * (ENG-5857) — empty at spawn is the designed state, not a failure.\n */\nexport const LATE_BOUND_VARS: ReadonlySet<string> = new Set([\n 'AGT_RUN_ID',\n 'AGT_TOKEN',\n 'ANCHOR_BROWSER_SESSION_ID',\n]);\n\nexport interface MissingSubstitutionVar {\n varName: string;\n /** Server key in `mcpServers` whose entry references the var. */\n server: string;\n state: 'unset' | 'empty';\n}\n\n/**\n * `${VAR}` with no default. `${VAR:-default}` can't fail substitution, so\n * the probe ignores it (same rule the Claude Code docs imply: only a\n * defaultless reference to an unset var is fatal).\n */\nconst TEMPLATE_VAR_RE = /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\n\nfunction collectVarsFromValue(value: unknown, into: Set<string>): void {\n if (typeof value === 'string') {\n for (const m of value.matchAll(TEMPLATE_VAR_RE)) into.add(m[1]!);\n } else if (Array.isArray(value)) {\n for (const v of value) collectVarsFromValue(v, into);\n }\n}\n\n/**\n * Find every defaultless `${VAR}` referenced by each MCP server entry\n * (env values, headers values, url, command, args) whose value in `env`\n * is unset or empty/whitespace. Returns one finding per (server, var).\n */\nexport function findMissingSubstitutionVars(\n mcpConfig: unknown,\n env: Record<string, string | undefined>,\n): MissingSubstitutionVar[] {\n const findings: MissingSubstitutionVar[] = [];\n if (typeof mcpConfig !== 'object' || mcpConfig === null) return findings;\n const servers = (mcpConfig as { mcpServers?: Record<string, unknown> }).mcpServers;\n if (typeof servers !== 'object' || servers === null) return findings;\n\n for (const [server, raw] of Object.entries(servers)) {\n if (typeof raw !== 'object' || raw === null) continue;\n const entry = raw as Record<string, unknown>;\n const vars = new Set<string>();\n collectVarsFromValue(entry['command'], vars);\n collectVarsFromValue(entry['args'], vars);\n collectVarsFromValue(entry['url'], vars);\n for (const block of [entry['env'], entry['headers']]) {\n if (typeof block !== 'object' || block === null) continue;\n for (const v of Object.values(block)) collectVarsFromValue(v, vars);\n }\n for (const varName of vars) {\n if (LATE_BOUND_VARS.has(varName)) continue;\n const value = env[varName];\n if (value === undefined) {\n findings.push({ varName, server, state: 'unset' });\n } else if (value.trim() === '') {\n findings.push({ varName, server, state: 'empty' });\n }\n }\n }\n return findings;\n}\n\n/** The structured, secret-free stderr line. */\nexport function formatMissingVar(f: MissingSubstitutionVar): string {\n return `[mcp-env-substitution] missing var=${f.varName} server=${f.server} state=${f.state}`;\n}\n\n/**\n * Parse a `.env.integrations` body with the same semantics as the\n * scheduled-task loader in manager-worker (skip blanks/comments, split on\n * first `=`) plus shell-quote stripping: the writer shell-quotes values\n * (`shellQuote`) because the persistent path `source`s the file, so a\n * Node-side reader must undo `'...'` wrapping to see the real value.\n */\nexport function parseEnvIntegrations(content: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const line of content.split('\\n')) {\n if (!line || line.startsWith('#') || !line.includes('=')) continue;\n const eqIdx = line.indexOf('=');\n const key = line.slice(0, eqIdx);\n let value = line.slice(eqIdx + 1);\n if (value.length >= 2 && value.startsWith(\"'\") && value.endsWith(\"'\")) {\n // shellQuote wraps in single quotes and escapes embedded ones as\n // `'\\''` — reverse both so the probe sees what the shell would.\n value = value.slice(1, -1).replaceAll(\"'\\\\''\", \"'\");\n }\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Convenience wrapper for spawn sites: read the rendered `.mcp.json` and\n * (optionally) `.env.integrations`, overlay the env file onto `baseEnv`\n * (mirroring what the wrapper's `source` / the scheduled-task loader\n * does), and return the findings. Never throws — a probe must not be\n * able to break a spawn.\n */\nexport function probeMcpEnvSubstitution(args: {\n mcpConfigPath: string;\n envIntegrationsPath?: string;\n baseEnv: Record<string, string | undefined>;\n}): MissingSubstitutionVar[] {\n try {\n const config = JSON.parse(readFileSync(args.mcpConfigPath, 'utf-8'));\n let env = args.baseEnv;\n if (args.envIntegrationsPath && existsSync(args.envIntegrationsPath)) {\n env = {\n ...args.baseEnv,\n ...parseEnvIntegrations(readFileSync(args.envIntegrationsPath, 'utf-8')),\n };\n }\n return findMissingSubstitutionVars(config, env);\n } catch {\n return [];\n }\n}\n","/**\n * ENG-4642: per-agent / per-day Claude session pinning.\n *\n * The persistent-session manager kills the tmux session on every spawn\n * (clean slate) and starts a fresh `claude` invocation. Pre-this-module,\n * that meant a new conversation every restart — operators lost context\n * any time the manager respawned.\n *\n * Goal: each calendar day is a fresh conversation, but every spawn\n * inside that day reuses the same conversation. We achieve this by\n * generating a stable UUID up front (Claude CLI accepts\n * `--session-id <uuid>` for the first spawn, `--resume <uuid>` for\n * subsequent ones) and persisting it to a tiny per-agent JSON file.\n *\n * Storage: `~/.augmented/<codeName>/daily-session.json` — same root the\n * persistent-session manager already owns via getProjectDir(). Schema:\n *\n * { \"date\": \"YYYY-MM-DD\", \"sessionId\": \"<uuid>\", \"history\": [...] }\n *\n * `history` keeps the last few days' entries so an operator can debug\n * which session was bound to which day. We trim to 7 days so the file\n * doesn't grow unbounded.\n *\n * Day boundary: defaults to host-local date (server timezone). Callers\n * may pass an IANA timezone (e.g. `Australia/Melbourne`) and the\n * rollover will fire at that zone's midnight instead — see ENG-5371.\n * The manager passes the agent's resolved `agentTimezone` (same source\n * as ENG-5363's channel MCP `TZ` env var: `teamSettings.timezone`,\n * defaulting to UTC) so the daily rollover lines up with what an\n * operator in the agent's timezone calls \"today\".\n *\n * Failure mode: if the on-disk JSONL Claude writes for the resumed\n * session is missing (host moved, profile wiped, claude version\n * incompatibility), `--resume` would fail and the agent would land on\n * the login picker. Callers verify the JSONL exists via\n * `sessionFileExists()` before choosing `--resume`; if it's gone we\n * fall back to `--session-id` (treat the stored UUID as fresh, claude\n * will materialise the JSONL on first turn).\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nconst HISTORY_DAYS = 7;\n\ninterface DailySessionEntry {\n date: string; // YYYY-MM-DD\n sessionId: string; // UUID v4\n startedAt: string; // ISO 8601\n}\n\ninterface DailySessionFile {\n current: DailySessionEntry | null;\n history: DailySessionEntry[];\n}\n\nexport interface DailySessionResult {\n sessionId: string;\n /** `true` when this call generated a new UUID (first spawn of a new day or first ever). */\n isNew: boolean;\n}\n\nfunction profileDir(codeName: string): string {\n return join(homedir(), '.augmented', codeName);\n}\n\nfunction dailySessionPath(codeName: string): string {\n return join(profileDir(codeName), 'daily-session.json');\n}\n\nfunction todayLocalIso(now: Date = new Date(), timezone?: string): string {\n // ENG-5371: when an IANA timezone is supplied (e.g. Australia/Melbourne),\n // compute the date in that zone via Intl.DateTimeFormat. Falling back to\n // Date getters preserves the original host-local behaviour for callers\n // (and tests) that don't supply a timezone — important for\n // backward-compatibility with the ENG-4642 contract.\n if (timezone) {\n try {\n const fmt = new Intl.DateTimeFormat('en-CA', {\n timeZone: timezone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n });\n // en-CA renders as `YYYY-MM-DD` already — no parts assembly needed.\n // Wrapped in try/catch in case the timezone string is invalid, in\n // which case we fall through to host-local rather than throw.\n return fmt.format(now);\n } catch {\n // Invalid IANA zone — fall back to host-local.\n }\n }\n const y = now.getFullYear();\n const m = String(now.getMonth() + 1).padStart(2, '0');\n const d = String(now.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nfunction readFile(codeName: string): DailySessionFile {\n const path = dailySessionPath(codeName);\n if (!existsSync(path)) return { current: null, history: [] };\n try {\n const raw = readFileSync(path, 'utf-8');\n const parsed = JSON.parse(raw) as Partial<DailySessionFile>;\n return {\n current: parsed.current ?? null,\n history: Array.isArray(parsed.history) ? parsed.history : [],\n };\n } catch {\n // Corrupt file — start fresh rather than crashing the manager.\n return { current: null, history: [] };\n }\n}\n\nfunction writeFile(codeName: string, data: DailySessionFile): void {\n const dir = profileDir(codeName);\n mkdirSync(dir, { recursive: true });\n // Atomic write: tmp + rename. A reader catching us mid-writeFileSync\n // would otherwise see truncated JSON and the corrupt-file branch in\n // readFile() would silently treat the agent as fresh state, losing\n // today's UUID and forcing a rollover the operator didn't ask for.\n // PID + randomUUID in the tmp suffix so two managers (or a respawn\n // racing with its predecessor) can't collide on the temp path and\n // have one rename remove the file the other is about to rename.\n // Mirrors the pattern in restart-flags.ts.\n const finalPath = dailySessionPath(codeName);\n const tmpPath = `${finalPath}.${process.pid}.${randomUUID()}.tmp`;\n writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');\n renameSync(tmpPath, finalPath);\n}\n\nfunction trimHistory(\n history: DailySessionEntry[],\n now: Date,\n timezone?: string,\n): DailySessionEntry[] {\n // Keep newest first, drop entries older than HISTORY_DAYS by date.\n // Take the injected `now` so callers with a frozen clock (tests\n // walking the day forward) don't get inconsistent cutoffs against\n // `new Date()`. The cutoff is computed in the same timezone as the\n // current entry's date string so equality comparisons hold across DST.\n const cutoff = new Date(now);\n cutoff.setDate(cutoff.getDate() - HISTORY_DAYS);\n const cutoffIso = todayLocalIso(cutoff, timezone);\n return history.filter((h) => h.date >= cutoffIso).slice(0, HISTORY_DAYS);\n}\n\n/**\n * Resolve the session UUID this agent should use right now. Generates\n * (and persists) a new UUID on the first call of a new local day, or\n * when the file is missing/corrupt; otherwise returns the day's\n * existing UUID. Idempotent within the same day.\n *\n * Concurrency: the read-then-write here is not under a file lock.\n * In our deployment the manager runs supervised, one process per\n * host (`agt manager start --supervise` / runSupervisorLoop), so\n * concurrent invocation for the same `codeName` is bounded to the\n * sub-second respawn window when the supervisor restarts the\n * worker. The atomic tmp+rename in writeFile() guarantees we never\n * read torn JSON, so the worst-case under a respawn race is two\n * managers minting different UUIDs and one rename winning — both\n * processes converge on the winner's UUID on the next supervisor\n * tick (which re-reads the file). We've taken that trade-off\n * over a proper inter-process lock because a stale lockfile (from\n * a SIGKILL'd manager) would block all subsequent runs and need\n * its own recovery path; the lossy outcome of a UUID race is one\n * tick of conversation churn, not a permanent block.\n */\nexport function getOrCreateDailySession(\n codeName: string,\n now: Date = new Date(),\n timezone?: string,\n): DailySessionResult {\n const today = todayLocalIso(now, timezone);\n const file = readFile(codeName);\n\n if (file.current && file.current.date === today) {\n return { sessionId: file.current.sessionId, isNew: false };\n }\n\n // Roll over: yesterday's (or older) entry moves to history, new one\n // takes its place.\n const next: DailySessionEntry = {\n date: today,\n sessionId: randomUUID(),\n startedAt: now.toISOString(),\n };\n const history = trimHistory(\n [...(file.current ? [file.current] : []), ...file.history],\n now,\n timezone,\n );\n writeFile(codeName, { current: next, history });\n return { sessionId: next.sessionId, isNew: true };\n}\n\n/**\n * Record the UUID a caller just spawned with so the day-rollover\n * marker (`current.date`) advances to today.\n *\n * ENG-5431: post-ENG-5397 every spawn mints a fresh `randomUUID()` in\n * `spawnSession()` and bypasses `getOrCreateDailySession`. Without this\n * helper, `daily-session.json` is never written on spawn, so\n * `isStaleForToday()` keeps returning true once the previous\n * `current.date` falls behind — re-firing the day-rollover restart on\n * every supervisor tick. Calling this after each spawn keeps the\n * marker in lockstep with the actual running session.\n *\n * Idempotent: re-calling with the same (date, sessionId) is a no-op\n * write of the same content. Different sessionId on the same date\n * just overwrites `current.sessionId` (the old one moves to history).\n */\nexport function markDailySessionSpawn(\n codeName: string,\n sessionId: string,\n now: Date = new Date(),\n timezone?: string,\n): void {\n const today = todayLocalIso(now, timezone);\n const file = readFile(codeName);\n if (file.current && file.current.date === today && file.current.sessionId === sessionId) {\n return;\n }\n const next: DailySessionEntry = {\n date: today,\n sessionId,\n startedAt: now.toISOString(),\n };\n const history = trimHistory(\n [...(file.current ? [file.current] : []), ...file.history],\n now,\n timezone,\n );\n writeFile(codeName, { current: next, history });\n}\n\n/**\n * Reset the day's pin — used as a recovery hatch after `--resume` is\n * rejected by claude (corrupt state, version mismatch). Writes a new\n * UUID for today, demotes the old one to history.\n */\nexport function rotateDailySession(\n codeName: string,\n now: Date = new Date(),\n timezone?: string,\n): string {\n const today = todayLocalIso(now, timezone);\n const file = readFile(codeName);\n const next: DailySessionEntry = {\n date: today,\n sessionId: randomUUID(),\n startedAt: now.toISOString(),\n };\n const history = trimHistory(\n [...(file.current ? [file.current] : []), ...file.history],\n now,\n timezone,\n );\n writeFile(codeName, { current: next, history });\n return next.sessionId;\n}\n\n/**\n * Encode an absolute project dir the way Claude Code stores it under\n * ~/.claude/projects/. Claude collapses runs of `/` and `.` into single\n * `-` separators with a leading `-` (no separator at the start). The\n * earlier \"/ only\" encoder produced a stale path for any project dir\n * containing a `.` (e.g. `/root/.augmented/scout/project`), which made\n * sessionFileExists() return false even when the JSONL was on disk.\n *\n * Diagnosed live on prod scout (ENG-4659): the on-disk dir was\n * /root/.claude/projects/-root--augmented-scout-project/\n * but our encoder produced\n * /root/.claude/projects/-root-.augmented-scout-project/\n * — the dot in `.augmented` wasn't translated. Result: every \"is\n * there JSONL on disk\" check returned false, the manager fell back to\n * `--session-id` reuse, and Claude rejected the same UUID with\n * \"Session ID already in use\" forever.\n *\n * Empirical observations from /root/.claude/projects/ on a live host:\n * /usr/bin -> -usr-bin\n * /root/.augmented/scout/project -> -root--augmented-scout-project\n * Behaviour: `[/.]` -> `-`, with consecutive separators preserved\n * (the `/.` between `root/` and `.augmented` becomes `--`).\n */\nfunction encodeProjectPath(projectDir: string): string {\n return '-' + projectDir.replace(/^\\//, '').replace(/[/.]/g, '-');\n}\n\n/**\n * Check whether claude has actually written a session JSONL for this\n * UUID. If the file is missing the `--resume` would fail and put the\n * agent on the login picker; callers should fall back to `--session-id`\n * instead. See encodeProjectPath() for the encoding rules.\n */\nexport function sessionFileExists(\n projectDir: string,\n sessionId: string,\n): boolean {\n const path = join(\n homedir(),\n '.claude',\n 'projects',\n encodeProjectPath(projectDir),\n `${sessionId}.jsonl`,\n );\n return existsSync(path);\n}\n\n/**\n * Directory under ~/.claude/projects/ where Claude Code stores every session\n * transcript for the given project dir. All of an agent's sessions —\n * persistent respawns (each a fresh UUID post-ENG-5397), scheduled tasks, and\n * direct-chat invocations — share this one directory because they all run with\n * the same cwd (getProjectDir). The token-usage monitor enumerates it.\n */\nexport function sessionTranscriptDir(projectDir: string): string {\n return join(homedir(), '.claude', 'projects', encodeProjectPath(projectDir));\n}\n\nexport function sessionFilePath(projectDir: string, sessionId: string): string {\n return join(sessionTranscriptDir(projectDir), `${sessionId}.jsonl`);\n}\n\n/**\n * Is the agent's session JSONL idle — i.e. has it not been written for\n * at least `idleSeconds`? Claude appends to the file on every turn\n * (tool calls, assistant messages, user messages) so a stale mtime is\n * a reliable proxy for \"nothing in flight\". Returns true if the file\n * is missing (no in-flight work to interrupt) or if its mtime is\n * older than the threshold.\n *\n * Used by the scheduled-rollover gate so we don't kill a tmux session\n * mid-task at the day boundary — defer the rollover one tick at a\n * time until the agent is between turns.\n */\nexport function isAgentIdle(\n projectDir: string,\n sessionId: string,\n idleSeconds = 60,\n now: Date = new Date(),\n): boolean {\n const path = sessionFilePath(projectDir, sessionId);\n if (!existsSync(path)) return true;\n try {\n const mtimeMs = statSync(path).mtimeMs;\n return now.getTime() - mtimeMs >= idleSeconds * 1000;\n } catch {\n // stat failed (race, permissions). Treat as non-idle to err on the\n // side of NOT interrupting a possibly-running task.\n return false;\n }\n}\n\n/**\n * Cheap \"should we roll over?\" check for the supervisor tick. Reads\n * the persisted current entry and compares its date against today's.\n * Does NOT mint a new UUID — the caller decides what to do with the\n * answer (typically: kill the tmux session iff isAgentIdle is true,\n * letting the next tick respawn fresh via getOrCreateDailySession).\n */\nexport function isStaleForToday(\n codeName: string,\n now: Date = new Date(),\n timezone?: string,\n): boolean {\n const file = readFile(codeName);\n if (!file.current) return false; // never seeded — nothing to roll\n return file.current.date !== todayLocalIso(now, timezone);\n}\n\n/**\n * Read-only accessor for the current entry, returns null when the\n * file doesn't exist or has no current entry. Useful to grab the\n * sessionId for the idle check without triggering a roll-over write.\n */\nexport function peekCurrentSession(codeName: string): {\n date: string;\n sessionId: string;\n startedAt: string;\n} | null {\n return readFile(codeName).current;\n}\n\n// Exported for unit tests — keep the surface small.\nexport const _internals = { todayLocalIso, dailySessionPath, profileDir, encodeProjectPath };\n","/**\n * ENG-5832 — Host-side agent-session liveness probe.\n *\n * This is the framework-agnostic, READ-ONLY primitive that answers \"is the\n * agent's interactive Claude session actually alive on this host right now?\"\n * by inspecting the local tmux server and process table. It is deliberately\n * separate from `../liveness/agent-liveness.ts` (ENG-4862), which derives a\n * UI-facing liveness state from heartbeat timestamps reported to the API —\n * that one is pure and browser-safe; THIS one shells out to `tmux`/`pgrep`\n * and is therefore node-only (exposed via the `@augmented/core/runtime/...`\n * subpath, never re-exported from the package barrel).\n *\n * Two consumers share this code so the subtle pgrep matching (ERE anchoring\n * + the `--` option-terminator guard CodeRabbit flagged) lives in one place:\n * - apps/cli `persistent-session.ts` — the manager's zombie detector, which\n * wraps these primitives in stateful bookkeeping (it also *kills* a dead\n * tmux shell; that mutation stays in the CLI).\n * - packages/mcp channel servers — to decide whether a freshly-arrived\n * inbound can actually be answered before acking it (see ack-reaction.ts).\n *\n * Tri-state by design. `execFileSync` throws both when a tool reports \"no\n * match\" (a real negative) AND when the tool is missing or times out (we\n * simply don't know). Collapsing those to a boolean is what makes a probe\n * dangerous to reuse: a host without `tmux`/`pgrep` would look uniformly\n * \"dead\" and every caller would over-react. We return 'unknown' for the\n * can't-tell cases so callers fail safe.\n */\n\nimport { execFileSync } from 'node:child_process';\n\n/** `alive` = confirmed present, `dead` = confirmed absent, `unknown` = couldn't determine. */\nexport type ProbeState = 'alive' | 'dead' | 'unknown';\n\n/**\n * tmux session name the manager uses when spawning an agent\n * (see persistent-session.ts `spawnSession`: `tmux new-session -s agt-<codeName>`).\n */\nexport function agentTmuxSessionName(codeName: string): string {\n return `agt-${codeName}`;\n}\n\n/**\n * Escape a string for safe embedding in a pgrep ERE pattern. tmux session\n * names are ASCII (`agt-<codeName>`) in practice, but `code_name` is external\n * input so we defensively neutralise every ERE metachar.\n */\nexport function escapePgrepRegex(value: string): string {\n return value.replace(/[.[\\]{}()*+?^$|\\\\]/g, '\\\\$&');\n}\n\n/**\n * Is a Claude process actually running inside the named tmux session?\n *\n * Matches on the `--name <tmuxSession>` argv pair the manager passes to claude\n * at spawn — the same flag `hasMcpChildren()` reuses successfully.\n *\n * - exit 0 / output → 'alive'\n * - exit 1 (no match) → 'dead'\n * - pgrep missing (ENOENT) / timeout / other → 'unknown'\n */\nexport function probeClaudeProcessInTmux(tmuxSession: string): ProbeState {\n // pgrep -f treats the pattern as an unanchored ERE against the full command\n // line. Without an end-boundary, `--name agt-foo` would match a claude\n // running as `--name agt-foobar` and we'd report a dead session as alive\n // (CodeRabbit). Anchor on whitespace/EOL either side so only the exact\n // `--name <tmuxSession>` argv pair matches.\n const escapedSession = escapePgrepRegex(tmuxSession);\n const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;\n try {\n // `--` ends pgrep's option parsing — the pattern itself begins with\n // `--name`, which would otherwise be parsed as a flag and produce either a\n // \"usage\" error or a silent no-match (verified on macOS).\n const out = execFileSync('pgrep', ['-f', '--', pattern], {\n encoding: 'utf-8',\n timeout: 3_000,\n }).trim();\n return out.length > 0 ? 'alive' : 'dead';\n } catch (err) {\n // execFileSync throws on a non-zero exit. pgrep exits 1 specifically when\n // nothing matched — that's an authoritative 'dead'. ENOENT (no pgrep) or\n // any other status (timeout-kill, usage error) is genuinely 'unknown'.\n const e = err as NodeJS.ErrnoException & { status?: number | null };\n if (e?.code === 'ENOENT') return 'unknown';\n return e?.status === 1 ? 'dead' : 'unknown';\n }\n}\n\n/**\n * Does the named tmux session exist?\n *\n * - exit 0 → 'alive'\n * - tmux missing (ENOENT) → 'unknown'\n * - any other failure → 'dead' (session absent)\n *\n * NOTE on the \"no server running\" ambiguity: `tmux has-session` also exits\n * non-zero when no tmux server is running at all, which is indistinguishable\n * here from \"server up, session gone\". Callers that can't otherwise tell\n * whether the agent is even tmux-managed MUST gate on `process.env.TMUX`\n * (present ⇒ a server is definitely running ⇒ non-zero genuinely means the\n * session is gone). See `probeAgentSessionGated`.\n */\nexport function probeTmuxSession(tmuxSession: string): ProbeState {\n try {\n execFileSync('tmux', ['has-session', '-t', tmuxSession], {\n stdio: 'ignore',\n timeout: 3_000,\n });\n return 'alive';\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e?.code === 'ENOENT') return 'unknown';\n return 'dead';\n }\n}\n\nexport interface SessionLiveness {\n tmux: ProbeState;\n claude: ProbeState;\n}\n\n/**\n * Combined read-only probe of an agent's interactive session. The claude\n * probe only runs when tmux is 'alive' — if the session shell is gone the\n * process question is moot, so claude inherits the tmux verdict.\n */\nexport function probeAgentSession(codeName: string): SessionLiveness {\n const session = agentTmuxSessionName(codeName);\n const tmux = probeTmuxSession(session);\n const claude = tmux === 'alive' ? probeClaudeProcessInTmux(session) : tmux;\n return { tmux, claude };\n}\n\ninterface ProbeCacheEntry {\n at: number;\n value: SessionLiveness;\n}\nconst probeCache = new Map<string, ProbeCacheEntry>();\n\n/** Default TTL — keeps the pgrep/tmux calls to roughly one per this window per agent. */\nexport const SESSION_PROBE_TTL_MS = 15_000;\n\n/**\n * `probeAgentSession` with a short TTL cache so a burst of inbound messages\n * doesn't fork a `tmux`/`pgrep` pair per message. The CLI manager uses its own\n * 30s cache; channel servers see higher inbound rates, so the shorter default\n * here trades a little freshness for staying well inside the detection target.\n *\n * @param now injectable clock for tests.\n */\nexport function probeAgentSessionCached(\n codeName: string,\n ttlMs: number = SESSION_PROBE_TTL_MS,\n now: number = Date.now(),\n): SessionLiveness {\n const cached = probeCache.get(codeName);\n if (cached && now - cached.at < ttlMs) return cached.value;\n const value = probeAgentSession(codeName);\n probeCache.set(codeName, { at: now, value });\n return value;\n}\n\n/** Test seam: drop the probe cache so each case starts clean. */\nexport function __resetSessionProbeCache(): void {\n probeCache.clear();\n}\n","/**\n * ENG-6017: shared Claude Code TUI dialog detection + dismissal.\n *\n * Extracted from persistent-session.ts's acceptDialogs() cascade so the\n * dialog knowledge has exactly one home, consumable from:\n *\n * - `acceptDialogs()` in persistent-session.ts — the spawn-time loop that\n * walks an agent through the first-run dialog cascade (theme → trust →\n * MCP → bypass …).\n * - `channel-input-watchdog.ts` — the per-poll-cycle watchdog that fires\n * Enter at stuck channel input. Before ENG-6017 the watchdog was\n * dialog-blind: when Claude Code's session-feedback dialog (\"How is\n * Claude doing this session?\") overlaid the pane, the watchdog's\n * single-shot Enter went into the dialog instead of the input box and\n * the inbound message sat unsubmitted for 40+ minutes (koda,\n * 2026-06-04) while every health metric stayed green.\n * - `injectMessageWithStatus()` — pre-send pane hygiene on the tmux\n * send-keys fallback path.\n *\n * Living in its own module (rather than persistent-session.ts) breaks the\n * import cycle persistent-session → channel-input-watchdog →\n * persistent-session that a shared export would otherwise create.\n *\n * DEFAULT-DENY: `sweepDialogs()` only ever returns an action for an\n * explicitly recognised dialog. An unknown dialog gets `null` — never a\n * blind Enter — because a future dialog's default option could be\n * destructive. Callers that want visibility into unknown overlays should\n * log the pane themselves.\n */\n\nimport { execFileSync } from 'node:child_process';\n\n/**\n * A recognised dialog plus the keystrokes that dismiss it.\n *\n * `keys` are tmux key names sent as individual `tmux send-keys` calls —\n * never batched into one call, so a multi-byte sequence can't get wrapped\n * into a single bracketed paste (the CSI-u trap documented on\n * defaultArmSender in persistent-session.ts).\n */\nexport interface DialogAction {\n kind:\n | 'theme-picker'\n | 'folder-trust'\n | 'resume-mode'\n | 'dev-channels'\n | 'mcp-servers'\n | 'bypass-permissions'\n | 'session-feedback';\n /** tmux key names, one send-keys invocation each. */\n keys: readonly string[];\n /** Delay between consecutive key sends (selector dialogs need a beat\n * between picking an option and confirming it). */\n interKeyDelayMs: number;\n /** Past-tense log fragment, e.g. \"Auto-accepted theme picker\". Callers\n * append their own context (`for '<codeName>'` etc.). */\n logMessage: string;\n}\n\n/**\n * Detect whether Claude Code is showing the **login picker** dialog.\n *\n * ENG-4634: this dialog appears when ~/.claude.json is missing or\n * Claude Code can't validate the saved session. Pressing Enter on\n * the default (1. Claude account with subscription) kicks off a\n * browser-based OAuth flow that an unattended agent can't complete —\n * the helper used to fall through to the generic `❯ no Enter to\n * confirm` exit branch and declare the session \"ready\" while the\n * actual claude REPL was still on the picker. Without explicit\n * detection, every manager respawn would silently flip the agent\n * back to the picker and never recover.\n *\n * Pattern matches the literal option strings claude renders. Both\n * 'Claude account with subscription' and 'Anthropic Console account'\n * are present on the picker (and not in the post-login UI), so the\n * conjunction is unambiguous.\n *\n * NOT part of sweepDialogs() — the picker must never be keyed past\n * (it needs an operator OAuth); acceptDialogs() handles it specially.\n */\nexport function isLoginPickerVisible(screen: string): boolean {\n return (\n screen.includes('Select login method') ||\n (screen.includes('Claude account with subscription') &&\n screen.includes('Anthropic Console account'))\n );\n}\n\n/**\n * Detect Claude Code's resume-mode dialog (ENG-5364).\n *\n * On `claude --resume <uuid>` against a transcript large enough to\n * trigger Claude Code 2.1.x's context-management heuristic, the agent\n * lands on an interactive picker offering:\n *\n * ❯ 1. Resume from summary (recommended)\n * 2. Resume full session as-is\n * 3. Don't ask me again\n *\n * Without auto-dismissal the agent sits silently waiting for keyboard\n * input on every manager respawn — channel inbounds stack up while\n * health metrics stay green. Surfaced fleet-wide on 2026-05-20\n * (don/stirling/maven hit it on a single manager restart).\n *\n * Match on the conjunction of two distinct option strings so a\n * passing mention of \"Resume\" in a transcript doesn't false-positive.\n */\nexport function isResumeModeDialogVisible(screen: string): boolean {\n return (\n screen.includes('Resume from summary') &&\n screen.includes(\"Don't ask me again\")\n );\n}\n\n/**\n * Detect Claude Code's session-feedback dialog (ENG-6017).\n *\n * After some turns Claude Code renders an optional rating prompt:\n *\n * ● How is Claude doing this session? (optional)\n * 1: Bad 2: Fine 3: Good 0: Dismiss\n *\n * It waits for a digit — Enter does nothing — so any injected message\n * sits in the input box unsubmitted, and the channel-input-watchdog's\n * Enter is swallowed too. Observed live on koda (agt-aws-1) 2026-06-04:\n * an operator Slack DM sat typed-but-unsubmitted for 40+ minutes behind\n * this dialog while pane-activity / synthetic-probe / heartbeat all\n * stayed green.\n *\n * Match the question text together with the literal `0: Dismiss` option\n * so a transcript merely *quoting* the question doesn't false-positive.\n */\nexport function isSessionFeedbackDialogVisible(screen: string): boolean {\n return (\n screen.includes('How is Claude doing this session') &&\n screen.includes('0: Dismiss')\n );\n}\n\n/**\n * Single-pass dialog recognition. Returns the dismissal action for the\n * first recognised dialog on screen, or `null` when no known dialog is\n * visible (including for the login picker, which must never be keyed\n * past — see isLoginPickerVisible).\n *\n * Branch order mirrors the original acceptDialogs() cascade: the theme\n * picker check must run before any generic `❯`-based readiness logic in\n * callers, since picker rows also render with `❯`.\n */\nexport function sweepDialogs(screen: string): DialogAction | null {\n if (\n screen.includes('Choose the text style') ||\n (screen.includes('Dark mode') && screen.includes('Light mode'))\n ) {\n return {\n kind: 'theme-picker',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted theme picker',\n };\n }\n if (screen.includes('Yes, I trust this folder')) {\n return {\n kind: 'folder-trust',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted folder trust',\n };\n }\n // ENG-5364: picks option 3 (\"Don't ask me again\") which Claude Code\n // persists in config, so subsequent resumes skip the dialog entirely.\n if (isResumeModeDialogVisible(screen)) {\n return {\n kind: 'resume-mode',\n keys: ['3', 'Enter'],\n interKeyDelayMs: 300,\n logMessage: \"Auto-dismissed resume-mode dialog (picked 'Don't ask me again')\",\n };\n }\n if (screen.includes('I am using this for local development')) {\n return {\n kind: 'dev-channels',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted dev channels',\n };\n }\n if (screen.includes('Enter to confirm') && screen.includes('MCP')) {\n return {\n kind: 'mcp-servers',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted MCP servers',\n };\n }\n if (screen.includes('Yes, I accept') && screen.includes('Bypass Permissions')) {\n return {\n kind: 'bypass-permissions',\n keys: ['2', 'Enter'],\n interKeyDelayMs: 300,\n logMessage: 'Auto-accepted bypass permissions',\n };\n }\n // ENG-6017: the rating prompt acts on the bare digit — no Enter needed.\n if (isSessionFeedbackDialogVisible(screen)) {\n return {\n kind: 'session-feedback',\n keys: ['0'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-dismissed session-feedback dialog',\n };\n }\n return null;\n}\n\n/**\n * Send a DialogAction's keystrokes to a tmux session, one send-keys call\n * per key with the action's inter-key delay. execFileSync (not execSync)\n * so the session name is an argv entry rather than shell-interpolated.\n */\nexport async function sendDialogKeys(\n tmuxSession: string,\n action: DialogAction,\n): Promise<void> {\n for (let i = 0; i < action.keys.length; i++) {\n if (i > 0 && action.interKeyDelayMs > 0) {\n await new Promise((r) => setTimeout(r, action.interKeyDelayMs));\n }\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, action.keys[i]!], {\n stdio: 'ignore',\n });\n }\n}\n\n/**\n * Tiny non-cryptographic hash for hash-only logging of channel input\n * (prod logging policy: input may contain PII/secrets, so log hash+len,\n * never content). Shared by the watchdog and the inject-time hygiene.\n */\nexport function simpleTextHash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) {\n h = ((h << 5) - h + s.charCodeAt(i)) | 0;\n }\n return h.toString(16);\n}\n","/**\n * ENG-4705: Channel input watchdog.\n *\n * Symptom: an inbound Slack/Telegram/Direct-Chat message lands in the Claude\n * Code TUI input buffer (visible as `❯ <text>` between the input-box rule\n * lines) but the channel server's auto-submit doesn't fire — the text just\n * sits there until something sends Enter manually.\n *\n * The dispatcher pattern landed in ENG-4684 reduces the *frequency* (slow\n * requests get fanned out to a background subagent so the parent's listener\n * turn returns immediately), but doesn't fix the underlying race: when\n * triage decides a request is \"fast\" and the parent handles it inline, the\n * main turn still occupies the TUI for several seconds, and any channel\n * message that arrives during that window stacks in the input buffer\n * un-submitted.\n *\n * Workaround the ticket itself documents: `tmux send-keys -t <session>\n * Enter`. This watchdog automates that — every poll cycle it captures the\n * pane for each managed claude-code agent, looks for un-submitted text in\n * the input box, and fires Enter once the same text has been sitting there\n * unchanged for STUCK_THRESHOLD_MS.\n *\n * Safety:\n * - Skip the agent if a tmux client is attached (a human might be typing).\n * - Require the text to be unchanged for STUCK_THRESHOLD_MS — short windows\n * avoid racing the channel server's own (eventual) submit.\n * - Bounded retries per stuck buffer (ENG-6017): up to MAX_ENTER_FIRES\n * Enters, each spaced by the stuck threshold, then a loud give-up log.\n * (The original single-shot design is exactly what stranded koda on\n * 2026-06-04: the one Enter went into Claude Code's session-feedback\n * dialog, the buffer was marked resolved, and an operator Slack DM sat\n * unsubmitted for 40+ minutes while every health metric stayed green.)\n * - Dialog-aware (ENG-6017): if a recognised dialog overlays the pane,\n * dismiss it via sweepDialogs() instead of firing Enter into it. The\n * Enter budget is not consumed by dialog dismissals. Unknown dialogs\n * are default-deny — never keyed.\n * - Skip if Claude is actively rendering a spinner (`✻ <verb>ing…`); a\n * fresh send-keys Enter into a busy TUI is harmless but noisy in logs.\n */\n\nimport { sweepDialogs, simpleTextHash, type DialogAction } from './claude-dialogs.js';\n\nconst STUCK_THRESHOLD_MS = 5_000;\n// ENG-6017: how many Enters we'll fire at one unchanged stuck buffer before\n// giving up loudly. Each retry waits another stuckThreshold after the last.\nconst MAX_ENTER_FIRES = 3;\n// ENG-4716: when a tmux client is attached we don't skip the agent\n// outright (the original safety-first carve-out swallowed the steady-\n// state \"monitoring\" case operators care about). Instead we widen the\n// stable-buffer window so a human typing has more than 5s to finish\n// before the watchdog steps in. Active typing changes the buffer hash\n// every keystroke and keeps resetting `firstSeenAt`, so this only\n// matters when the buffer is *truly* stable.\nconst ATTACHED_STUCK_THRESHOLD_MS = 15_000;\nconst INPUT_BOX_DIVIDER = /^[─━]{10,}/;\nconst PROMPT_PREFIX = '❯ ';\n\nexport interface AgentInputState {\n /** Hash of the input-box text observed last poll. */\n lastInputHash: string;\n /** Wall-clock ms when this hash was first seen. */\n firstSeenAt: number;\n /** ENG-6017: how many Enters we've fired at this hash so far. */\n fires: number;\n /** Wall-clock ms of the most recent Enter fire (0 = never). */\n lastFireAt: number;\n /** Have we already emitted the give-up log line for this hash? */\n gaveUpLogged: boolean;\n}\n\nexport interface WatchdogIo {\n /** Snapshot of the agent's tmux pane (multiline). Empty / null if the session doesn't exist. */\n capturePane: (codeName: string) => string | null;\n /** Whether any tmux client is attached to the session right now. */\n isClientAttached: (codeName: string) => boolean;\n /** Send a single Enter keystroke to the agent's tmux session. */\n sendEnter: (codeName: string) => void;\n /**\n * ENG-6017: send a dialog-dismissal key sequence (one send-keys call per\n * key, `interKeyDelayMs` apart) to the agent's tmux session.\n */\n sendKeys: (codeName: string, keys: readonly string[], interKeyDelayMs: number) => void;\n /** Logger. */\n log: (msg: string) => void;\n /** Current wall-clock ms (injectable for tests). */\n now: () => number;\n}\n\nexport interface WatchdogConfig {\n /** ms a stuck buffer must persist before we fire Enter. Defaults to STUCK_THRESHOLD_MS. */\n stuckThresholdMs?: number;\n /**\n * ms a stuck buffer must persist before we fire Enter when a tmux\n * client is attached to the session. Higher than `stuckThresholdMs`\n * to give a human typer extra headroom. Defaults to\n * ATTACHED_STUCK_THRESHOLD_MS.\n */\n attachedStuckThresholdMs?: number;\n /**\n * ENG-6017: max Enters fired at one unchanged stuck buffer before the\n * loud give-up log. Defaults to MAX_ENTER_FIRES.\n */\n maxEnterFires?: number;\n}\n\n/**\n * Single-agent decision step — pure given the pane text + state. Returns\n * the next state and the action to take: dismiss a dialog, fire Enter,\n * emit the one-shot give-up log, or nothing.\n *\n * Exported for unit testing.\n */\nexport function decide(\n pane: string,\n prev: AgentInputState | undefined,\n now: number,\n config: WatchdogConfig = {},\n): {\n fire: boolean;\n /** ENG-6017: a recognised dialog overlays the pane — dismiss it instead\n * of firing Enter into it. The Enter budget is untouched. */\n dialog?: DialogAction;\n /** ENG-6017: the Enter budget for this hash is exhausted — emit the\n * give-up log exactly once. */\n gaveUp?: boolean;\n next: AgentInputState | undefined;\n} {\n const threshold = config.stuckThresholdMs ?? STUCK_THRESHOLD_MS;\n const maxFires = config.maxEnterFires ?? MAX_ENTER_FIRES;\n\n // ENG-6017: dialogs first. While a recognised dialog overlays the pane,\n // an Enter would land on the dialog (the koda incident: the session-\n // feedback prompt ate the watchdog's single shot). Dismiss it and leave\n // the stuck-input state untouched — next cycle sees the clean pane and\n // the normal Enter logic resumes. Unknown overlays fall through to the\n // standard path (default-deny: we never key what we don't recognise;\n // worst case the bounded Enters fire and the give-up log surfaces it).\n const dialogAction = sweepDialogs(pane);\n if (dialogAction) {\n return { fire: false, dialog: dialogAction, next: prev };\n }\n\n const inputText = extractInputBoxText(pane);\n if (!inputText) {\n return { fire: false, next: undefined };\n }\n\n if (isActivelyProcessing(pane)) {\n // Don't reset the timer — the input might still be stuck once Claude\n // finishes; we just don't fire while a spinner is live.\n return { fire: false, next: prev };\n }\n\n const hash = simpleTextHash(inputText);\n if (!prev || prev.lastInputHash !== hash) {\n return {\n fire: false,\n next: { lastInputHash: hash, firstSeenAt: now, fires: 0, lastFireAt: 0, gaveUpLogged: false },\n };\n }\n\n // ENG-6017: bounded retries replace the original single-shot `resolved`\n // flag. An Enter that lands while a dialog is mid-render (or is eaten by\n // a TUI mode hiccup) gets another chance, each spaced by the same stuck\n // threshold; after maxFires the watchdog surfaces a loud give-up line —\n // the signature monitoring hooks onto — instead of silently shrugging.\n if (prev.fires >= maxFires) {\n if (!prev.gaveUpLogged) {\n return { fire: false, gaveUp: true, next: { ...prev, gaveUpLogged: true } };\n }\n return { fire: false, next: prev };\n }\n\n const sinceLastAttempt = prev.fires === 0 ? now - prev.firstSeenAt : now - prev.lastFireAt;\n if (sinceLastAttempt < threshold) return { fire: false, next: prev };\n\n return {\n fire: true,\n next: { ...prev, fires: prev.fires + 1, lastFireAt: now },\n };\n}\n\n/**\n * Extract the contents of the Claude Code input box from a pane snapshot.\n * Returns null when the input box is empty or absent.\n *\n * The TUI bracketed input area looks like:\n * ────────── agt-bob ──\n * ❯ ping repro 2\n * ──────────────────────\n *\n * We accept any line starting with `❯ ` whose previous non-empty line is a\n * row of `─` characters (the top divider) — that's robust to width changes\n * and to the optional ` agt-<codeName> ` label embedded in the divider.\n *\n * Exported for unit testing.\n */\nexport function extractInputBoxText(pane: string): string | null {\n const lines = pane.split('\\n');\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i] ?? '';\n if (!line.startsWith(PROMPT_PREFIX)) continue;\n // Walk back to the most recent non-empty line; it must be a divider row.\n let j = i - 1;\n while (j >= 0 && (lines[j] ?? '').trim() === '') j--;\n if (j < 0) continue;\n if (!INPUT_BOX_DIVIDER.test((lines[j] ?? '').trim())) continue;\n const text = line.slice(PROMPT_PREFIX.length).trim();\n return text.length > 0 ? text : null;\n }\n return null;\n}\n\n/**\n * True when the pane shows a live spinner (`✻ Cogitating…`,\n * `✻ Crunching…`, etc.). Past tense forms like `✻ Cogitated for 7s` mean\n * the work has finished and we treat the agent as idle.\n *\n * Exported for unit testing.\n */\nexport function isActivelyProcessing(pane: string): boolean {\n // Search bottom-up for the most recent spinner line.\n const lines = pane.split('\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = (lines[i] ?? '').trim();\n if (!line.startsWith('✻')) continue;\n // Past tense: \"✻ Cogitated for 7s\", \"✻ Crunched for 51s\" — work done.\n if (/\\bfor\\s+\\d+s\\s*$/.test(line)) return false;\n // Present participle: \"✻ Cogitating…\", \"✻ Crunching…\" — still working.\n if (/\\b\\w+ing[…\\.]{0,3}\\s*$/i.test(line)) return true;\n // Ambiguous spinner line — don't block on it.\n return false;\n }\n return false;\n}\n\n/**\n * Run one watchdog pass over the given agents. Stateful: keeps an internal\n * map of per-agent input state across calls.\n */\nexport function checkChannelInputs(\n codeNames: readonly string[],\n io: WatchdogIo,\n config: WatchdogConfig = {},\n states: Map<string, AgentInputState> = sharedStates,\n): void {\n const live = new Set(codeNames);\n for (const codeName of codeNames) {\n try {\n checkOne(codeName, io, config, states);\n } catch (err) {\n io.log(`[channel-input-watchdog] '${codeName}': ${(err as Error).message}`);\n }\n }\n // Drop state for agents that are no longer in scope.\n for (const key of [...states.keys()]) {\n if (!live.has(key)) states.delete(key);\n }\n // Keep give-up counters in sync with live agents as well — a stale entry\n // would mis-attribute an old count if a code name is reused (CodeRabbit\n // on PR #1764).\n for (const key of [...giveUpCounts.keys()]) {\n if (!live.has(key)) giveUpCounts.delete(key);\n }\n}\n\nfunction checkOne(\n codeName: string,\n io: WatchdogIo,\n config: WatchdogConfig,\n states: Map<string, AgentInputState>,\n): void {\n const pane = io.capturePane(codeName);\n if (!pane) {\n states.delete(codeName);\n return;\n }\n\n // ENG-4716: don't skip outright when a client is attached — operators\n // routinely keep a tmux client open just to monitor agents, and the\n // original blanket skip ate the steady-state case. Widen the stable-\n // buffer threshold instead. Active typing changes the buffer hash on\n // every keystroke, so the unchanged-hash gate already covers the\n // \"don't fight the human\" concern.\n const attached = io.isClientAttached(codeName);\n const effectiveConfig: WatchdogConfig = attached\n ? {\n ...config,\n stuckThresholdMs:\n config.attachedStuckThresholdMs ?? ATTACHED_STUCK_THRESHOLD_MS,\n }\n : config;\n\n const prev = states.get(codeName);\n const { fire, dialog, gaveUp, next } = decide(pane, prev, io.now(), effectiveConfig);\n\n if (next === undefined) {\n states.delete(codeName);\n } else {\n states.set(codeName, next);\n }\n\n if (dialog) {\n // ENG-6017: a recognised dialog is blocking the input box — dismiss it\n // instead of firing Enter into it. Next cycle re-evaluates the clean pane.\n io.log(\n `[channel-input-watchdog] '${codeName}': ${dialog.logMessage} (dialog was blocking the input box)`,\n );\n io.sendKeys(codeName, dialog.keys, dialog.interKeyDelayMs);\n return;\n }\n\n // Log hash + length only — channel input may contain PII / secrets, so\n // prod logging stays hash-only per the project's logging policy.\n const text = extractInputBoxText(pane) ?? '';\n const hash = next?.lastInputHash ?? simpleTextHash(text);\n\n if (gaveUp) {\n // ENG-6017: loud, single-shot per hash. This line is the fast-detection\n // signature for \"input typed but unsubmittable\" — the failure mode every\n // upstream health metric is blind to (koda 2026-06-04).\n const maxFires = effectiveConfig.maxEnterFires ?? MAX_ENTER_FIRES;\n io.log(\n `[channel-input-watchdog] '${codeName}': GIVING UP after ${maxFires} Enter attempts — input remains unsubmitted (input_hash=${hash}, len=${text.length})`,\n );\n // Count the event for the responsiveness probe (InputStuckGiveUps\n // metric) so the give-up reaches CloudWatch on the next probe cycle,\n // not just the local log.\n giveUpCounts.set(codeName, (giveUpCounts.get(codeName) ?? 0) + 1);\n return;\n }\n\n if (fire) {\n const maxFires = effectiveConfig.maxEnterFires ?? MAX_ENTER_FIRES;\n io.log(\n `[channel-input-watchdog] '${codeName}': stuck channel input — firing Enter (attempt ${next?.fires ?? 1}/${maxFires}, input_hash=${hash}, len=${text.length})`,\n );\n io.sendEnter(codeName);\n }\n}\n\nconst sharedStates = new Map<string, AgentInputState>();\n\n// ENG-6017: per-agent count of give-up events since the last responsiveness\n// probe drained them. Consumed by the manager's probe cycle and shipped to\n// CloudWatch as `InputStuckGiveUps` — the fast-detection metric for the\n// \"typed but unsubmittable\" failure mode (each event means a channel message\n// sat in the input box through every bounded Enter retry).\nconst giveUpCounts = new Map<string, number>();\n\n/**\n * Drain the give-up counter for one agent (returns the count since the last\n * drain). Read-and-reset so each probe cycle reports only new events.\n */\nexport function takeWatchdogGiveUpCount(codeName: string): number {\n const count = giveUpCounts.get(codeName) ?? 0;\n giveUpCounts.delete(codeName);\n return count;\n}\n\n/** Test seam — clear the singleton map between tests. */\nexport function _resetSharedStatesForTests(): void {\n sharedStates.clear();\n giveUpCounts.clear();\n}\n"],"mappings":"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","names":["execFileSync","join","homedir","existsSync","readFileSync","writeFileSync","mkdirSync","readFileSync","randomUUID","existsSync","readFileSync","writeFileSync","existsSync","execFileSync","existsSync","join","mkdirSync","writeFileSync","readFileSync","execFileSync","homedir","randomUUID","session"]}
|