@integrity-labs/agt-cli 0.25.2 → 0.26.0
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 +4 -4
- package/dist/{chunk-M2RBM3M5.js → chunk-LULDCIUS.js} +5 -66
- package/dist/chunk-LULDCIUS.js.map +1 -0
- package/dist/{chunk-U4DAVDAZ.js → chunk-XG4X7BNT.js} +2 -2
- package/dist/{chunk-VWOXS4YP.js → chunk-YMIDZT5T.js} +1 -5
- package/dist/chunk-YMIDZT5T.js.map +1 -0
- package/dist/{claude-pair-runtime-Q5CZLM2R.js → claude-pair-runtime-7PHOEAUN.js} +2 -2
- package/dist/lib/manager-worker.js +21 -104
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/{persistent-session-MUJA45LT.js → persistent-session-3ITHPO4P.js} +3 -9
- package/dist/{responsiveness-probe-2TBRHC5T.js → responsiveness-probe-7XWORQLO.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-M2RBM3M5.js.map +0 -1
- package/dist/chunk-VWOXS4YP.js.map +0 -1
- /package/dist/{chunk-U4DAVDAZ.js.map → chunk-XG4X7BNT.js.map} +0 -0
- /package/dist/{claude-pair-runtime-Q5CZLM2R.js.map → claude-pair-runtime-7PHOEAUN.js.map} +0 -0
- /package/dist/{persistent-session-MUJA45LT.js.map → persistent-session-3ITHPO4P.js.map} +0 -0
- /package/dist/{responsiveness-probe-2TBRHC5T.js.map → responsiveness-probe-7XWORQLO.js.map} +0 -0
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
_internals,
|
|
3
3
|
collectDiagnostics,
|
|
4
|
-
ensureKanbanLoopArmed,
|
|
5
4
|
getLastFailureContext,
|
|
6
5
|
getProjectDir,
|
|
7
6
|
getSessionState,
|
|
8
7
|
injectMessage,
|
|
9
8
|
injectMessageWithStatus,
|
|
10
9
|
isAgentPromptReady,
|
|
11
|
-
isKanbanLoopArmedForCurrentSession,
|
|
12
|
-
isKanbanLoopDisabled,
|
|
13
10
|
isSessionHealthy,
|
|
14
11
|
paneLogPath,
|
|
15
12
|
prepareForRespawn,
|
|
@@ -23,20 +20,17 @@ import {
|
|
|
23
20
|
stopPersistentSession,
|
|
24
21
|
takeZombieDetection,
|
|
25
22
|
writePersistentClaudeWrapper
|
|
26
|
-
} from "./chunk-
|
|
27
|
-
import "./chunk-
|
|
23
|
+
} from "./chunk-LULDCIUS.js";
|
|
24
|
+
import "./chunk-YMIDZT5T.js";
|
|
28
25
|
export {
|
|
29
26
|
_internals,
|
|
30
27
|
collectDiagnostics,
|
|
31
|
-
ensureKanbanLoopArmed,
|
|
32
28
|
getLastFailureContext,
|
|
33
29
|
getProjectDir,
|
|
34
30
|
getSessionState,
|
|
35
31
|
injectMessage,
|
|
36
32
|
injectMessageWithStatus,
|
|
37
33
|
isAgentPromptReady,
|
|
38
|
-
isKanbanLoopArmedForCurrentSession,
|
|
39
|
-
isKanbanLoopDisabled,
|
|
40
34
|
isSessionHealthy,
|
|
41
35
|
paneLogPath,
|
|
42
36
|
prepareForRespawn,
|
|
@@ -51,4 +45,4 @@ export {
|
|
|
51
45
|
takeZombieDetection,
|
|
52
46
|
writePersistentClaudeWrapper
|
|
53
47
|
};
|
|
54
|
-
//# sourceMappingURL=persistent-session-
|
|
48
|
+
//# sourceMappingURL=persistent-session-3ITHPO4P.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
paneLogPath
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-LULDCIUS.js";
|
|
4
|
+
import "./chunk-YMIDZT5T.js";
|
|
5
5
|
|
|
6
6
|
// src/lib/responsiveness-probe.ts
|
|
7
7
|
import { statSync } from "fs";
|
|
@@ -29,4 +29,4 @@ export {
|
|
|
29
29
|
collectResponsivenessProbes,
|
|
30
30
|
getResponsivenessIntervalMs
|
|
31
31
|
};
|
|
32
|
-
//# sourceMappingURL=responsiveness-probe-
|
|
32
|
+
//# sourceMappingURL=responsiveness-probe-7XWORQLO.js.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/persistent-session.ts","../src/lib/mcp-sanitize.ts","../src/lib/claude-tools.ts","../src/lib/orphan-channel-mcp-reaper.ts","../src/lib/daily-session.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 { claudeModelAlias } 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';\nimport { KANBAN_LOOP_COMMAND } from '@augmented/core';\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// 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 * ENG-5404: the `currentSessionId` we successfully armed the in-session\n * `/loop kanban-work` against. `armKanbanLoop()` re-arms whenever this\n * differs from `currentSessionId`, which means:\n * - fresh spawn (null → uuid) → arms\n * - daily rotation (uuid-A → uuid-B) → re-arms\n * - same-session retry after success → no-op (idempotent)\n * - failed arm injection (stays unset) → retried next poll tick\n *\n * In-memory only. On manager restart against an alive tmux session\n * this resets to null and the arm fires again — that's safe because\n * Claude Code's `/loop` for the same cadence replaces any prior loop\n * rather than stacking. If observation shows it's worth saving an arm\n * round-trip on manager restarts, this can be persisted to\n * manager-state.json in a follow-up.\n */\n loopArmedSessionId: 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 loopArmedSessionId: existing?.loopArmedSessionId ?? 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 // 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, then arm the in-session kanban-work\n // /loop (ENG-5404). Chained via .then so the arm doesn't fire until\n // the prompt has cleared the dialog cascade. Both rejections swallow\n // — the manager polls and `armKanbanLoop` is idempotent, so any\n // transient failure recovers on the next tick.\n acceptDialogs(tmuxSession, codeName, log)\n .then(() => armKanbanLoop(tmuxSession, codeName, session, log))\n .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/**\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 */\nfunction 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 */\nfunction 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 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(tmuxSession: string, codeName: string, log: (msg: string) => void): 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 // First-run theme picker. Has to be dismissed BEFORE the\n // generic \"❯ no Enter to confirm\" exit branch below, since the\n // picker's selected row also renders with `❯`. Accept the\n // highlighted default (Dark mode); operator can change later\n // via /theme.\n if (\n screen.includes('Choose the text style') ||\n (screen.includes('Dark mode') && screen.includes('Light mode'))\n ) {\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted theme picker for '${codeName}'`);\n continue;\n }\n if (screen.includes('Yes, I trust this folder')) {\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted workspace trust for '${codeName}'`);\n continue;\n }\n // ENG-5364: Claude Code 2.1.x resume-mode dialog. Picks option 3\n // (\"Don't ask me again\") which Claude Code persists in config,\n // so subsequent resumes for this agent skip the dialog entirely.\n // Without this the agent silently waits for keyboard ENTER on\n // every manager respawn against a long-running transcript.\n if (isResumeModeDialogVisible(screen)) {\n // execFileSync (not execSync) so tmuxSession is passed as an\n // argv entry rather than interpolated into a shell command.\n // The surrounding file still uses shell-string execSync in\n // several other places (out of scope for ENG-5364); we use the\n // safer pattern for the new code path.\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, '3'], { stdio: 'ignore' });\n await new Promise((r) => setTimeout(r, 300));\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, 'Enter'], { stdio: 'ignore' });\n log(\n `[persistent-session] Auto-dismissed resume-mode dialog for '${codeName}' (picked 'Don't ask me again')`,\n );\n continue;\n }\n if (screen.includes('I am using this for local development')) {\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted dev channels for '${codeName}'`);\n continue;\n }\n if (screen.includes('Enter to confirm') && screen.includes('MCP')) {\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted MCP servers for '${codeName}'`);\n continue;\n }\n if (screen.includes('Yes, I accept') && screen.includes('Bypass Permissions')) {\n execSync(`tmux send-keys -t ${tmuxSession} 2`, { stdio: 'ignore' });\n await new Promise((r) => setTimeout(r, 300));\n execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: 'ignore' });\n log(`[persistent-session] Auto-accepted bypass permissions 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 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// In-session /loop arming (ENG-5404)\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 * Arm the in-session `/loop kanban-work` command on the agent's REPL.\n *\n * Replaces the external `claude -p kanban-work` cron with a single\n * bootstrap-time injection that the Claude Code REPL self-paces from then\n * on. See `docs/research/kanban-work-in-session-loop.md` and the\n * KANBAN_LOOP_COMMAND constant in `@augmented/core`.\n *\n * Idempotency: re-arm only when `session.loopArmedSessionId` differs from\n * `session.currentSessionId`. Fresh spawn (null → uuid) arms; daily\n * rotation (uuid-A → uuid-B) re-arms; same-session retry no-ops. A failed\n * arm leaves `loopArmedSessionId` unchanged so the next manager poll\n * retries automatically.\n *\n * IMPORTANT: pass `meta` as `undefined` to `injectMessage()` — passing a\n * `task_name` would prepend `[Task: …] ` to the content, pushing the\n * leading `/` off column 1. Claude Code only parses `/` as a slash\n * command at the start of a line, so the prefix would silently downgrade\n * the arm to a plain user message. See ENG-5404 for the gotcha.\n */\n/**\n * Type of the function used by armKanbanLoop to deliver the /loop string\n * to the agent's tmux session. 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\nfunction defaultArmSender(tmuxSession: string, command: string): boolean {\n try {\n // execFileSync (not execSync) so the tmuxSession and command are\n // passed as argv entries rather than interpolated into a shell.\n //\n // The two-step send (command string, then 'Enter' as a separate\n // arg) is what makes Claude Code's REPL parse `/` as a slash\n // command instead of plain user text. The earlier acpx path\n // delivered the string as a file-loaded user message which\n // arrived downstream of the keystroke-layer slash parser, so\n // /loop was treated as literal content and no timer was ever\n // scheduled. See post-mortem in docs/research/kanban-work-in-\n // session-loop.md (rollout-2026-05-22 section).\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, command, '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-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/**\n * ENG-5433: operator kill switch for the in-session `/loop kanban-work`.\n * Sibling of `isKanbanWorkCronDisabled()` in manager-worker.ts (ENG-5405)\n * which gates the legacy `claude -p kanban-work` cron path.\n *\n * Set `AGT_DISABLE_KANBAN_LOOP=1` (or `=true`, case-insensitive) in\n * `/etc/agt/agt-manager.env` (or any equivalent EnvironmentFile) and\n * restart the manager. `armKanbanLoop()` then short-circuits at every\n * call site — both the spawn-time arm and the steady-state retry — so\n * no `/loop` ever gets injected for new sessions.\n *\n * Already-running loops on an existing tmux pane keep going until the\n * agent's next session restart; the gate only controls whether new\n * sessions re-arm.\n *\n * Read on every call so flipping the var only requires a manager\n * restart (not a CLI re-bundle).\n */\nexport function isKanbanLoopDisabled(): boolean {\n const v = process.env['AGT_DISABLE_KANBAN_LOOP'];\n return v === '1' || v?.toLowerCase() === 'true';\n}\n\n/**\n * Dedup set so the \"disabled via env\" log line fires once per\n * (agent, sessionId) rather than on every poll tick. Cleared when the\n * test suite resets module state via `_internals.__resetKanbanLoopGateState()`.\n */\nconst kanbanLoopDisabledLogged = new Set<string>();\n\nasync function armKanbanLoop(\n tmuxSession: string,\n codeName: string,\n session: PersistentSession,\n log: (msg: string) => void,\n): Promise<void> {\n // ENG-5433: env kill switch. Log once per session id so an operator\n // can confirm the gate is firing without flooding the log on every\n // poll tick. `loopArmedSessionId` stays untouched so its semantics\n // (\"we've actually armed the loop on this session\") remain clean.\n if (isKanbanLoopDisabled()) {\n const dedupKey = `${codeName}:${session.currentSessionId ?? 'pre-spawn'}`;\n if (!kanbanLoopDisabledLogged.has(dedupKey)) {\n log(\n `[persistent-session] /loop kanban-work disabled for '${codeName}' via AGT_DISABLE_KANBAN_LOOP`,\n );\n kanbanLoopDisabledLogged.add(dedupKey);\n }\n return;\n }\n\n // Idempotency gate — short-circuit before any tmux interaction so this\n // helper is cheap to call on every manager poll tick.\n if (session.currentSessionId == null) return;\n if (session.loopArmedSessionId === session.currentSessionId) return;\n\n const ready = await waitForPromptReady(tmuxSession);\n if (!ready) {\n log(\n `[persistent-session] /loop arm skipped for '${codeName}' — prompt not ready within 10s, will retry next poll`,\n );\n return;\n }\n\n // KANBAN_LOOP_COMMAND must be a single line — see the docstring in\n // packages/core/src/loops/kanban-loop.ts. Multi-line input would type\n // embedded newlines as Enter and submit the partial command.\n const armed = armSender(tmuxSession, KANBAN_LOOP_COMMAND);\n if (armed) {\n session.loopArmedSessionId = session.currentSessionId;\n log(`[persistent-session] Armed /loop kanban-work for '${codeName}'`);\n } else {\n log(\n `[persistent-session] Failed to arm /loop for '${codeName}' — will retry next poll`,\n );\n }\n}\n\n/**\n * Re-try the kanban-work /loop arm on a healthy steady-state tick.\n *\n * The spawn-time arm (chained off `acceptDialogs`) handles the common case\n * — fresh spawn arms once and we're done. This helper covers the rarer\n * \"spawn succeeded but arm failed\" case (acpx hiccup, prompt-not-ready\n * race) by being safe to call from the manager poll loop on every tick.\n * Short-circuits when already armed (the `loopArmedSessionId` check), so\n * the steady-state cost is one Map lookup + one string compare.\n *\n * Returns silently on unknown agent, not-yet-running session, or already\n * armed — every branch is a no-op the caller can ignore.\n */\nexport async function ensureKanbanLoopArmed(\n codeName: string,\n log: (msg: string) => void,\n): Promise<void> {\n const session = sessions.get(codeName);\n if (!session || session.status !== 'running') return;\n if (session.currentSessionId == null) return;\n if (session.loopArmedSessionId === session.currentSessionId) return;\n const tmuxSession = `agt-${codeName}`;\n await armKanbanLoop(tmuxSession, codeName, session, log);\n}\n\n/**\n * CodeRabbit PR #1275: cheap synchronous check used by the manager-\n * worker cron-skip path. Returns true only when the in-session /loop\n * has been successfully armed against the current session UUID — i.e.,\n * it is safe to suppress the legacy `claude -p kanban-work` cron\n * because the loop will handle board polling. False on (a) unknown\n * agent, (b) not-yet-running session, (c) currentSessionId still null,\n * (d) armed against an older session UUID (daily rotation between\n * spawn and now), (e) arm injection failed. The cron-skip falls back\n * to firing the legacy task in any of those cases so the board never\n * goes unattended.\n */\nexport function isKanbanLoopArmedForCurrentSession(codeName: string): boolean {\n const session = sessions.get(codeName);\n if (!session || session.status !== 'running') return false;\n if (session.currentSessionId == null) return false;\n return session.loopArmedSessionId === session.currentSessionId;\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 armKanbanLoop,\n waitForPromptReady,\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 // Test-only resets so each test starts from a clean slate.\n __resetZombieState(): void {\n zombieProbeCache.clear();\n pendingZombieDetections.clear();\n },\n // ENG-5433: clear the disabled-log dedup set between tests so the\n // \"logs once per (agent, session)\" assertion is deterministic.\n __resetKanbanLoopGateState(): void {\n kanbanLoopDisabledLogged.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).\n // Fire-and-forget — spawn detached so the manager loop isn't blocked.\n const acpx = getAcpxBin();\n if (acpx) {\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 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 {\n _log(`[inject] acpx binary not found — falling back to tmux send-keys`);\n }\n\n // Fallback: tmux send-keys (may have paste issues with long text)\n // Use execFileSync to avoid shell injection — text passed as literal arg\n try {\n execFileSync('tmux', ['send-keys', '-t', `agt-${codeName}`, text, 'Enter'], { stdio: 'ignore' });\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 } catch (err) {\n _log(`[inject] tmux send-keys failed for '${codeName}': ${(err as Error).message}`);\n return { delivered: false, fallbackUsed: false };\n }\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\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/**\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\n * external input so we defensively neutralise every ERE metachar.\n */\nfunction escapePgrepRegex(value: string): string {\n return value.replace(/[.[\\]{}()*+?^$|\\\\]/g, '\\\\$&');\n}\n\nfunction isClaudeProcessAliveInTmux(tmuxSession: string): boolean {\n // Match on the --name flag the manager passes to claude at spawn.\n // pgrep exits 1 when nothing matches — treat that as \"dead\", along with\n // any other error (timeout, missing pgrep on hostile environments).\n try {\n // pgrep -f treats the pattern as an unanchored ERE against the full\n // command line. Without an end-boundary, `--name agt-foo` would match\n // a claude running as `--name agt-foobar` and we'd report a dead\n // session as alive (CodeRabbit). Anchor on whitespace/EOL either side\n // so only the exact `--name <tmuxSession>` argv pair matches.\n const escapedSession = escapePgrepRegex(tmuxSession);\n const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;\n // `--` ends pgrep's option parsing — the pattern itself begins with\n // `--name`, which would otherwise be parsed as a flag and produce\n // either a \"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;\n } catch {\n return false;\n }\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 loopArmedSessionId: 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 and Agent. Their absence disables integration-skill activation and\n// subagent dispatch without warning. Keep that list in one place so a new\n// spawn site physically cannot miss them.\n\n// Order is stable for test snapshots.\nconst BASE_TOOLS = ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'Agent', 'Skill'] 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 eight base built-ins.\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","// ENG-4808: reap channel-MCP processes (telegram-channel.js, slack-channel.js,\n// direct-chat-channel.js) that are still alive but whose parent claude\n// session has exited. Without this, every agent restart leaves a tree of\n// long-polling orphans behind — each holds the agent's bot token, eats\n// memory, and can race the live MCP for inbound updates.\n//\n// The decision is \"PPID-chain to a live claude\" — if a channel-MCP's\n// process ancestry reaches a `claude` process with `--name agt-<codeName>`\n// in argv, it's live. Otherwise it's an orphan and gets reaped.\n//\n// This module exposes:\n// • findOrphanChannelMcps() — pure, takes parsed `ps` rows + returns\n// orphan PIDs. Trivially unit-testable.\n// • reapOrphanChannelMcps() — side-effecting. Walks ps, then SIGTERMs\n// and after a grace window SIGKILLs any orphans.\n//\n// macOS / Linux: both support `ps -eo pid,ppid,args` with the format we\n// parse here. Windows is not a target for the agent host.\n\nimport { execFileSync } from 'node:child_process';\n\nexport interface PsRow {\n pid: number;\n ppid: number;\n args: string;\n}\n\n/** Match the three channel-MCPs the manager spawns into agent sessions. */\nexport const CHANNEL_MCP_PATTERN =\n /\\.augmented\\/_mcp\\/(telegram-channel|slack-channel|direct-chat-channel)\\.js/;\n\n/** Match a claude process bound to an agent session via `--name agt-<x>`. */\nexport const CLAUDE_AGENT_PROCESS_PATTERN = /\\bclaude\\b.*--name\\s+agt-[a-z0-9-]+/;\n\n/**\n * Parse `ps -eo pid,ppid,args` output (header + one row per process).\n * Whitespace-tolerant; the `args` column contains the rest of the line\n * after the second whitespace-delimited token.\n */\nexport function parsePsRows(psOutput: string): PsRow[] {\n const lines = psOutput.split(/\\r?\\n/);\n const rows: PsRow[] = [];\n // Skip header line(s) — anything where pid isn't a positive integer.\n for (const raw of lines) {\n const line = raw.trim();\n if (!line) continue;\n const match = line.match(/^(\\d+)\\s+(\\d+)\\s+(.*)$/);\n if (!match) continue;\n const pid = Number.parseInt(match[1]!, 10);\n const ppid = Number.parseInt(match[2]!, 10);\n if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;\n rows.push({ pid, ppid, args: match[3] ?? '' });\n }\n return rows;\n}\n\n/**\n * Pure: given a list of `ps` rows, return the PIDs of channel-MCP\n * processes whose ancestry doesn't reach a live `claude --name agt-*`\n * process. \"Orphan\" = parent claude has exited (so the MCP can no\n * longer deliver `notifications/claude/channel` to anyone).\n *\n * Walks the ppid chain up to `maxDepth` levels (default 8) so an\n * intermediate shell or wrapper doesn't trick us into a false orphan\n * verdict. Stops early on a hit to keep this O(N * maxDepth) at worst.\n */\nexport function findOrphanChannelMcps(rows: PsRow[], opts?: { maxDepth?: number }): number[] {\n const maxDepth = opts?.maxDepth ?? 8;\n const byPid = new Map<number, PsRow>(rows.map((r) => [r.pid, r]));\n\n const orphans: number[] = [];\n for (const row of rows) {\n if (!CHANNEL_MCP_PATTERN.test(row.args)) continue;\n\n // Walk parents looking for a live claude.\n let cur: PsRow | undefined = byPid.get(row.ppid);\n let foundLiveClaude = false;\n for (let depth = 0; depth < maxDepth && cur; depth++) {\n if (CLAUDE_AGENT_PROCESS_PATTERN.test(cur.args)) {\n foundLiveClaude = true;\n break;\n }\n // PID 1 (init) is always reached on Linux for true orphans —\n // stop the walk regardless to bound work.\n if (cur.pid === 1) break;\n cur = byPid.get(cur.ppid);\n }\n\n if (!foundLiveClaude) orphans.push(row.pid);\n }\n\n return orphans;\n}\n\n/**\n * Run `ps` and reap any orphan channel-MCPs. SIGTERM first, then SIGKILL\n * after `graceMs` for any that didn't exit. Logs each reaped pid so\n * operators see what was cleaned up.\n *\n * Idempotent: calling repeatedly is safe (the second call will find\n * nothing).\n *\n * Returns the list of pids that were sent SIGTERM (informational —\n * tests can use this without mocking process.kill).\n */\nexport function reapOrphanChannelMcps(args: {\n log: (msg: string) => void;\n graceMs?: number;\n /** Test seam: substitute the `ps` invocation. */\n runPs?: () => string;\n /** Test seam: substitute the kill function. */\n killProcess?: (pid: number, signal: 'SIGTERM' | 'SIGKILL') => void;\n /** Test seam: substitute the still-alive check. */\n isAlive?: (pid: number) => boolean;\n}): number[] {\n const { log, graceMs = 5_000 } = args;\n const runPs = args.runPs ?? (() => execFileSync('ps', ['-eo', 'pid,ppid,args'], { encoding: 'utf-8', timeout: 5_000 }));\n const killProcess =\n args.killProcess ??\n ((pid: number, signal: 'SIGTERM' | 'SIGKILL') => {\n try {\n process.kill(pid, signal);\n } catch {\n // Process already exited — fine.\n }\n });\n const isAlive =\n args.isAlive ??\n ((pid: number) => {\n try {\n // Signal 0 doesn't kill anything — just probes whether the pid\n // is reachable. Throws if no such process.\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n });\n\n let psOutput: string;\n try {\n psOutput = runPs();\n } catch (err) {\n log(`[orphan-reaper] ps invocation failed: ${(err as Error).message} — skipping reap`);\n return [];\n }\n\n const rows = parsePsRows(psOutput);\n const orphans = findOrphanChannelMcps(rows);\n if (orphans.length === 0) return [];\n\n // CodeRabbit on PR #774: surface the script name in log lines so an\n // operator scanning manager.log can immediately tell which channel-MCP\n // was reaped without cross-referencing PIDs against `ps` output. The\n // AC explicitly asked for `script.js (pid N)` format.\n const byPid = new Map<number, PsRow>(rows.map((r) => [r.pid, r]));\n const describeOrphan = (pid: number): string => {\n const scriptMatch = byPid.get(pid)?.args.match(/([^\\s/]+\\.js)(?:\\s|$)/);\n return scriptMatch ? `${scriptMatch[1]} (pid ${pid})` : `pid ${pid}`;\n };\n\n log(`[orphan-reaper] sending SIGTERM to ${orphans.length} orphan channel-MCP(s): ${orphans.map(describeOrphan).join(', ')}`);\n for (const pid of orphans) {\n killProcess(pid, 'SIGTERM');\n }\n\n // Schedule a SIGKILL pass for any that didn't exit. Don't block the\n // caller — this is purely cleanup hygiene. CodeRabbit on PR #774:\n // wrap the body so a throwing test seam (custom isAlive / killProcess)\n // doesn't escape as an uncaught exception. Production seams already\n // catch internally, so this is purely defence-in-depth for tests.\n setTimeout(() => {\n try {\n const stragglers = orphans.filter(isAlive);\n if (stragglers.length === 0) return;\n log(`[orphan-reaper] ${stragglers.length} orphan(s) survived SIGTERM; sending SIGKILL: ${stragglers.map(describeOrphan).join(', ')}`);\n for (const pid of stragglers) {\n killProcess(pid, 'SIGKILL');\n }\n } catch (err) {\n log(`[orphan-reaper] error in SIGKILL pass: ${(err as Error).message}`);\n }\n }, graceMs).unref();\n\n return orphans;\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"],"mappings":"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","names":["execFileSync","join","homedir","existsSync","readFileSync","writeFileSync","mkdirSync","randomUUID","readFileSync","writeFileSync","existsSync","join","mkdirSync","writeFileSync","readFileSync","homedir","randomUUID","execFileSync","session"]}
|