@integrity-labs/agt-cli 0.28.1142 → 0.28.1144

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.
Files changed (25) hide show
  1. package/dist/bin/agt.js +13 -11
  2. package/dist/bin/agt.js.map +1 -1
  3. package/dist/{chunk-J2PUVUK6.js → chunk-C33IQH4H.js} +20 -16
  4. package/dist/{chunk-J2PUVUK6.js.map → chunk-C33IQH4H.js.map} +1 -1
  5. package/dist/{chunk-ZJFS74H5.js → chunk-EZYLIFK2.js} +24 -22
  6. package/dist/{chunk-ZJFS74H5.js.map → chunk-EZYLIFK2.js.map} +1 -1
  7. package/dist/{chunk-JM47BARM.js → chunk-FQD4EIUF.js} +2 -233
  8. package/dist/chunk-FQD4EIUF.js.map +1 -0
  9. package/dist/chunk-XAZ4C444.js +238 -0
  10. package/dist/chunk-XAZ4C444.js.map +1 -0
  11. package/dist/{claude-code-updater-NEJCTT7S.js → claude-code-updater-HTDCRIMM.js} +99 -4
  12. package/dist/claude-code-updater-HTDCRIMM.js.map +1 -0
  13. package/dist/{claude-pair-runtime-A3OWKP3K.js → claude-pair-runtime-VEDZCIJR.js} +2 -2
  14. package/dist/lib/manager-worker.js +43 -37
  15. package/dist/lib/manager-worker.js.map +1 -1
  16. package/dist/{persistent-session-VNJDG3VN.js → persistent-session-JKD36BCM.js} +4 -3
  17. package/dist/{responsiveness-probe-ABQGETFV.js → responsiveness-probe-GGBDYMEW.js} +4 -3
  18. package/dist/{responsiveness-probe-ABQGETFV.js.map → responsiveness-probe-GGBDYMEW.js.map} +1 -1
  19. package/dist/{session-auth-dead-DVE6OGRT.js → session-auth-dead-7AG56OKQ.js} +3 -2
  20. package/dist/{session-auth-dead-DVE6OGRT.js.map → session-auth-dead-7AG56OKQ.js.map} +1 -1
  21. package/package.json +1 -1
  22. package/dist/chunk-JM47BARM.js.map +0 -1
  23. package/dist/claude-code-updater-NEJCTT7S.js.map +0 -1
  24. /package/dist/{claude-pair-runtime-A3OWKP3K.js.map → claude-pair-runtime-VEDZCIJR.js.map} +0 -0
  25. /package/dist/{persistent-session-VNJDG3VN.js.map → persistent-session-JKD36BCM.js.map} +0 -0
@@ -0,0 +1,238 @@
1
+ import {
2
+ encodeClaudeProjectPath
3
+ } from "./chunk-FQD4EIUF.js";
4
+
5
+ // src/lib/daily-session.ts
6
+ import { randomUUID } from "crypto";
7
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ var HISTORY_DAYS = 7;
11
+ function profileDir(codeName) {
12
+ return join(homedir(), ".augmented", codeName);
13
+ }
14
+ function dailySessionPath(codeName) {
15
+ return join(profileDir(codeName), "daily-session.json");
16
+ }
17
+ function todayLocalIso(now = /* @__PURE__ */ new Date(), timezone) {
18
+ if (timezone) {
19
+ try {
20
+ const fmt = new Intl.DateTimeFormat("en-CA", {
21
+ timeZone: timezone,
22
+ year: "numeric",
23
+ month: "2-digit",
24
+ day: "2-digit"
25
+ });
26
+ return fmt.format(now);
27
+ } catch {
28
+ }
29
+ }
30
+ const y = now.getFullYear();
31
+ const m = String(now.getMonth() + 1).padStart(2, "0");
32
+ const d = String(now.getDate()).padStart(2, "0");
33
+ return `${y}-${m}-${d}`;
34
+ }
35
+ function readFile(codeName) {
36
+ const path = dailySessionPath(codeName);
37
+ if (!existsSync(path)) return { current: null, history: [] };
38
+ try {
39
+ const raw = readFileSync(path, "utf-8");
40
+ const parsed = JSON.parse(raw);
41
+ return {
42
+ current: parsed.current ?? null,
43
+ history: Array.isArray(parsed.history) ? parsed.history : []
44
+ };
45
+ } catch {
46
+ return { current: null, history: [] };
47
+ }
48
+ }
49
+ function writeFile(codeName, data) {
50
+ const dir = profileDir(codeName);
51
+ mkdirSync(dir, { recursive: true });
52
+ const finalPath = dailySessionPath(codeName);
53
+ const tmpPath = `${finalPath}.${process.pid}.${randomUUID()}.tmp`;
54
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
55
+ renameSync(tmpPath, finalPath);
56
+ }
57
+ function trimHistory(history, now, timezone) {
58
+ const cutoff = new Date(now);
59
+ cutoff.setDate(cutoff.getDate() - HISTORY_DAYS);
60
+ const cutoffIso = todayLocalIso(cutoff, timezone);
61
+ return history.filter((h) => h.date >= cutoffIso).slice(0, HISTORY_DAYS);
62
+ }
63
+ function getOrCreateDailySession(codeName, now = /* @__PURE__ */ new Date(), timezone) {
64
+ const today = todayLocalIso(now, timezone);
65
+ const file = readFile(codeName);
66
+ if (file.current && file.current.date === today) {
67
+ return { sessionId: file.current.sessionId, isNew: false };
68
+ }
69
+ const next = {
70
+ date: today,
71
+ sessionId: randomUUID(),
72
+ startedAt: now.toISOString()
73
+ };
74
+ const history = trimHistory(
75
+ [...file.current ? [file.current] : [], ...file.history],
76
+ now,
77
+ timezone
78
+ );
79
+ writeFile(codeName, { current: next, history });
80
+ return { sessionId: next.sessionId, isNew: true };
81
+ }
82
+ function markDailySessionSpawn(codeName, sessionId, now = /* @__PURE__ */ new Date(), timezone) {
83
+ const today = todayLocalIso(now, timezone);
84
+ const file = readFile(codeName);
85
+ if (file.current && file.current.date === today && file.current.sessionId === sessionId) {
86
+ return;
87
+ }
88
+ const next = {
89
+ date: today,
90
+ sessionId,
91
+ startedAt: now.toISOString()
92
+ };
93
+ const history = trimHistory(
94
+ [...file.current ? [file.current] : [], ...file.history],
95
+ now,
96
+ timezone
97
+ );
98
+ writeFile(codeName, { current: next, history });
99
+ }
100
+ function rotateDailySession(codeName, now = /* @__PURE__ */ new Date(), timezone) {
101
+ const today = todayLocalIso(now, timezone);
102
+ const file = readFile(codeName);
103
+ const next = {
104
+ date: today,
105
+ sessionId: randomUUID(),
106
+ startedAt: now.toISOString()
107
+ };
108
+ const history = trimHistory(
109
+ [...file.current ? [file.current] : [], ...file.history],
110
+ now,
111
+ timezone
112
+ );
113
+ writeFile(codeName, { current: next, history });
114
+ return next.sessionId;
115
+ }
116
+ var encodeProjectPath = encodeClaudeProjectPath;
117
+ function sessionFileExists(projectDir, sessionId) {
118
+ const path = join(
119
+ homedir(),
120
+ ".claude",
121
+ "projects",
122
+ encodeProjectPath(projectDir),
123
+ `${sessionId}.jsonl`
124
+ );
125
+ return existsSync(path);
126
+ }
127
+ function sessionTranscriptDir(projectDir) {
128
+ return join(homedir(), ".claude", "projects", encodeProjectPath(projectDir));
129
+ }
130
+ function sessionFilePath(projectDir, sessionId) {
131
+ return join(sessionTranscriptDir(projectDir), `${sessionId}.jsonl`);
132
+ }
133
+ function transcriptActivityAgeSeconds(projectDir, sessionId, now = /* @__PURE__ */ new Date()) {
134
+ if (!sessionId) return null;
135
+ try {
136
+ const mtimeMs = statSync(sessionFilePath(projectDir, sessionId)).mtimeMs;
137
+ return Math.max(0, Math.floor((now.getTime() - mtimeMs) / 1e3));
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ function subagentActivityAgeSeconds(projectDir, sessionId, now = /* @__PURE__ */ new Date()) {
143
+ if (!sessionId) return null;
144
+ const dir = join(sessionTranscriptDir(projectDir), sessionId, "subagents");
145
+ try {
146
+ let freshestMtimeMs = null;
147
+ for (const name of readdirSync(dir)) {
148
+ if (!name.endsWith(".jsonl")) continue;
149
+ try {
150
+ const mtimeMs = statSync(join(dir, name)).mtimeMs;
151
+ if (freshestMtimeMs === null || mtimeMs > freshestMtimeMs) freshestMtimeMs = mtimeMs;
152
+ } catch {
153
+ }
154
+ }
155
+ if (freshestMtimeMs === null) return null;
156
+ return Math.max(0, Math.floor((now.getTime() - freshestMtimeMs) / 1e3));
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+ function isAgentIdle(projectDir, sessionId, idleSeconds = 60, now = /* @__PURE__ */ new Date()) {
162
+ const path = sessionFilePath(projectDir, sessionId);
163
+ if (!existsSync(path)) return true;
164
+ try {
165
+ const mtimeMs = statSync(path).mtimeMs;
166
+ return now.getTime() - mtimeMs >= idleSeconds * 1e3;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ function isStaleForToday(codeName, now = /* @__PURE__ */ new Date(), timezone) {
172
+ const file = readFile(codeName);
173
+ if (!file.current) return false;
174
+ return file.current.date !== todayLocalIso(now, timezone);
175
+ }
176
+ function minutesSinceLocalMidnight(now = /* @__PURE__ */ new Date(), timezone) {
177
+ if (timezone) {
178
+ try {
179
+ const fmt = new Intl.DateTimeFormat("en-GB", {
180
+ timeZone: timezone,
181
+ hour: "2-digit",
182
+ minute: "2-digit",
183
+ hourCycle: "h23"
184
+ });
185
+ const parts = fmt.formatToParts(now);
186
+ const hh = Number(parts.find((p) => p.type === "hour")?.value ?? "0");
187
+ const mm = Number(parts.find((p) => p.type === "minute")?.value ?? "0");
188
+ if (Number.isFinite(hh) && Number.isFinite(mm)) return hh * 60 + mm;
189
+ } catch {
190
+ }
191
+ }
192
+ return now.getHours() * 60 + now.getMinutes();
193
+ }
194
+ function peekCurrentSession(codeName) {
195
+ return readFile(codeName).current;
196
+ }
197
+ function readDailySessionPin(codeName) {
198
+ const path = dailySessionPath(codeName);
199
+ if (!existsSync(path)) {
200
+ return { path, present: false, raw: null, currentDate: null, currentSessionId: null };
201
+ }
202
+ let raw;
203
+ try {
204
+ raw = readFileSync(path, "utf-8");
205
+ } catch {
206
+ return { path, present: true, raw: null, currentDate: null, currentSessionId: null };
207
+ }
208
+ try {
209
+ const parsed = JSON.parse(raw);
210
+ const current = parsed.current ?? null;
211
+ return {
212
+ path,
213
+ present: true,
214
+ raw,
215
+ currentDate: current?.date ?? null,
216
+ currentSessionId: current?.sessionId ?? null
217
+ };
218
+ } catch {
219
+ return { path, present: true, raw, currentDate: null, currentSessionId: null };
220
+ }
221
+ }
222
+
223
+ export {
224
+ todayLocalIso,
225
+ getOrCreateDailySession,
226
+ markDailySessionSpawn,
227
+ rotateDailySession,
228
+ sessionFileExists,
229
+ sessionTranscriptDir,
230
+ transcriptActivityAgeSeconds,
231
+ subagentActivityAgeSeconds,
232
+ isAgentIdle,
233
+ isStaleForToday,
234
+ minutesSinceLocalMidnight,
235
+ peekCurrentSession,
236
+ readDailySessionPin
237
+ };
238
+ //# sourceMappingURL=chunk-XAZ4C444.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/daily-session.ts"],"sourcesContent":["/**\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, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { encodeClaudeProjectPath } from '@augmented/core';\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\nexport function 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: a spawn that bypasses `getOrCreateDailySession` (today the\n * AGT_DISABLE_SESSION_RESUME path, which mints a fresh `randomUUID()`;\n * under ENG-5397 it was every spawn) never writes `daily-session.json`,\n * so `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. On the ENG-6039\n * resume/fresh paths it's an idempotent no-op.\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/.\n *\n * ENG-8201 (Slice 1) moved the rule itself to `@augmented/core`\n * (`encodeClaudeProjectPath`) so the channel MCP servers can locate the same\n * transcript directory without a second copy of it — see that function for the\n * encoding rules and the ENG-4659 incident that produced them. This stays as a\n * local alias so the module's callers (and `_internals`) read unchanged.\n */\nconst encodeProjectPath = encodeClaudeProjectPath;\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 (one pinned UUID per agent-tz day, ENG-6039, plus\n * rotations), scheduled tasks, and direct-chat invocations — share this one\n * directory because they all run with the same cwd (getProjectDir). The\n * 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 * ENG-6238: age (s) of the current session's transcript JSONL — the wedge\n * detector's \"is the model actually producing tokens right now\" signal. The\n * transcript grows as the model streams turns/tool calls, so a fresh mtime\n * means real work is happening, where pane.log can be kept fresh by a frozen\n * but animated spinner. Returns null when there's no session id or the file\n * can't be stat'd (the detector then degrades to the pane-age fallback).\n */\nexport function transcriptActivityAgeSeconds(\n projectDir: string,\n sessionId: string | null,\n now: Date = new Date(),\n): number | null {\n if (!sessionId) return null;\n try {\n const mtimeMs = statSync(sessionFilePath(projectDir, sessionId)).mtimeMs;\n return Math.max(0, Math.floor((now.getTime() - mtimeMs) / 1000));\n } catch {\n return null;\n }\n}\n\n/**\n * ENG-6294: age (s) of the freshest sub-agent transcript for the current\n * session, or null when there is none. Claude Code writes each sub-agent's\n * transcript to `<transcriptDir>/<sessionId>/subagents/agent-<id>.jsonl` —\n * so while a `run_in_background` worker grinds, ITS jsonl keeps growing even\n * though the parent ended its turn and the main `<sessionId>.jsonl` goes\n * static. A fresh mtime here means the session has a background task doing\n * real work and must not read as wedged (the ENG-6274 dispatch flow's\n * signature is exactly quiet-pane + stale-main-transcript + queued inbound).\n */\nexport function subagentActivityAgeSeconds(\n projectDir: string,\n sessionId: string | null,\n now: Date = new Date(),\n): number | null {\n if (!sessionId) return null;\n const dir = join(sessionTranscriptDir(projectDir), sessionId, 'subagents');\n try {\n let freshestMtimeMs: number | null = null;\n for (const name of readdirSync(dir)) {\n if (!name.endsWith('.jsonl')) continue;\n try {\n const mtimeMs = statSync(join(dir, name)).mtimeMs;\n if (freshestMtimeMs === null || mtimeMs > freshestMtimeMs) freshestMtimeMs = mtimeMs;\n } catch {\n // file vanished between readdir and stat — skip it\n }\n }\n if (freshestMtimeMs === null) return null;\n return Math.max(0, Math.floor((now.getTime() - freshestMtimeMs) / 1000));\n } catch {\n return null; // no subagents dir for this session\n }\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 * Minutes elapsed since the local (agent-timezone) start of today, i.e. how\n * far past midnight we are in the agent's day. ENG-7548: the scheduled\n * day-rollover gate defers while an agent is mid-task; a perpetually-busy\n * agent never presents an idle window, so the rollover would defer\n * indefinitely. The caller uses this \"how overdue is today's rollover\"\n * measure to bound the defer — once it exceeds a grace period the rollover is\n * forced even if the agent looks busy. Timezone-aware via Intl (hourCycle\n * 'h23' so midnight is 00, not 24); falls back to host-local on an invalid\n * zone, mirroring todayLocalIso().\n */\nexport function minutesSinceLocalMidnight(\n now: Date = new Date(),\n timezone?: string,\n): number {\n if (timezone) {\n try {\n const fmt = new Intl.DateTimeFormat('en-GB', {\n timeZone: timezone,\n hour: '2-digit',\n minute: '2-digit',\n hourCycle: 'h23',\n });\n const parts = fmt.formatToParts(now);\n const hh = Number(parts.find((p) => p.type === 'hour')?.value ?? '0');\n const mm = Number(parts.find((p) => p.type === 'minute')?.value ?? '0');\n if (Number.isFinite(hh) && Number.isFinite(mm)) return hh * 60 + mm;\n } catch {\n // Invalid IANA zone — fall back to host-local.\n }\n }\n return now.getHours() * 60 + now.getMinutes();\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/**\n * ENG-9566: snapshot of the per-agent session pin, for archiving on drain.\n *\n * WHY THIS IS ARCHIVED AT ALL. `resolveSessionSpawnDecision` only `--resume`s a\n * conversation when a transcript exists for *today's pinned UUID*. That pin\n * lives here, at `~/.augmented/<codeName>/daily-session.json`, and it is swept\n * by none of the three archivers: agt-session-archiver.sh covers\n * `/root/.claude/projects` plus a `-maxdepth 1 -type f -name 'manager.log-*'`\n * sweep of `/root/.augmented` (whose comment excludes per-agent subdirectories\n * explicitly), and the drain flush covers `~/.claude/projects`. So a destination\n * host that restored every transcript byte-for-byte would still mint a fresh\n * `randomUUID()` and treat all of them as orphans. Archiving the pin alongside\n * the transcripts is what makes a restored conversation resumable at all.\n *\n * Under the id-keyed layout `~/.augmented/<codeName>` is a symlink to the real\n * `~/.augmented/<agentId>` dir, so reading through the codename path lands on\n * the same file either way — no layout branch is needed here.\n *\n * `raw` is kept verbatim so a later schema change loses nothing; the two parsed\n * fields are the convenience a restore actually reads. A corrupt or unreadable\n * file keeps `present: true` and nulls the parsed fields rather than reporting\n * absence — \"unparseable\" and \"never existed\" are different problems for\n * whoever debugs a failed restore, and collapsing them hides the first.\n */\nexport interface DailySessionPinSnapshot {\n /** Absolute path the pin was read from (recorded for debugging a restore). */\n path: string;\n /** The file exists. False means this agent has no pin at all. */\n present: boolean;\n /** Verbatim file contents; null when present but unreadable. */\n raw: string | null;\n /** Parsed `current.date` (YYYY-MM-DD in the agent's timezone), if any. */\n currentDate: string | null;\n /** Parsed `current.sessionId` — the UUID a destination must `--resume`. */\n currentSessionId: string | null;\n}\n\nexport function readDailySessionPin(codeName: string): DailySessionPinSnapshot {\n const path = dailySessionPath(codeName);\n if (!existsSync(path)) {\n return { path, present: false, raw: null, currentDate: null, currentSessionId: null };\n }\n let raw: string;\n try {\n raw = readFileSync(path, 'utf-8');\n } catch {\n // Unreadable (permissions, or a teardown race) — present but uncaptured.\n return { path, present: true, raw: null, currentDate: null, currentSessionId: null };\n }\n try {\n const parsed = JSON.parse(raw) as Partial<DailySessionFile>;\n const current = parsed.current ?? null;\n return {\n path,\n present: true,\n raw,\n currentDate: current?.date ?? null,\n currentSessionId: current?.sessionId ?? null,\n };\n } catch {\n // Corrupt JSON: keep the bytes so a human can still recover the UUID.\n return { path, present: true, raw, currentDate: null, currentSessionId: null };\n }\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":[]}
@@ -2,6 +2,9 @@ import {
2
2
  CLAUDE_AGENT_PROCESS_PATTERN,
3
3
  parsePsRows
4
4
  } from "./chunk-XWVM4KPK.js";
5
+ import {
6
+ FORCED_UPDATE_HARD_DEADLINE_MS
7
+ } from "./chunk-FQD4EIUF.js";
5
8
  import {
6
9
  effectiveClaudeJsonPath
7
10
  } from "./chunk-EL5MWGGH.js";
@@ -14,6 +17,8 @@ import { homedir } from "os";
14
17
  import { promisify } from "util";
15
18
  var execFileAsync = promisify(execFile);
16
19
  var NPM_PKG = "@anthropic-ai/claude-code";
20
+ var CLAUDE_CODE_SECURITY_FLOOR = "2.1.308";
21
+ var BELOW_FLOOR_RECHECK_MS = 6e4;
17
22
  var DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1e3;
18
23
  var DEFAULT_VERIFY_MISMATCH_THRESHOLD = 5;
19
24
  function intervalMs() {
@@ -31,6 +36,32 @@ function parseVersion(output) {
31
36
  function isConcreteVersion(spec) {
32
37
  return /^\d+\.\d+\.\d+[A-Za-z0-9.+-]*$/.test(spec.trim());
33
38
  }
39
+ function compareVersions(a, b) {
40
+ const re = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/;
41
+ const ma = re.exec(a.trim());
42
+ const mb = re.exec(b.trim());
43
+ if (!ma || !mb) return null;
44
+ for (let i = 1; i <= 3; i++) {
45
+ const d = Number(ma[i]) - Number(mb[i]);
46
+ if (d !== 0) return d < 0 ? -1 : 1;
47
+ }
48
+ const preA = ma[4] !== void 0;
49
+ const preB = mb[4] !== void 0;
50
+ if (preA === preB) return 0;
51
+ return preA ? -1 : 1;
52
+ }
53
+ function shouldRunClaudeCodeUpdater(input) {
54
+ if (!input.desiredVersion) return false;
55
+ if (isBelowSecurityFloor(input.installedVersion, input.floor)) return true;
56
+ return input.windowGate === "proceed";
57
+ }
58
+ function isBelowSecurityFloor(version, floor = CLAUDE_CODE_SECURITY_FLOOR) {
59
+ if (!version) return false;
60
+ const parsed = parseVersion(version);
61
+ if (!parsed) return false;
62
+ const cmp = compareVersions(parsed, floor);
63
+ return cmp !== null && cmp < 0;
64
+ }
34
65
  var WELL_KNOWN_CLAUDE_PATHS = [
35
66
  "/usr/bin/claude",
36
67
  // Anthropic native autoupdater (Linux deb/rpm)
@@ -44,10 +75,12 @@ var WELL_KNOWN_CLAUDE_PATHS = [
44
75
  var lastCheckAt = 0;
45
76
  var multiInstallWarned = false;
46
77
  var verifyMismatchState = null;
78
+ var belowFloorBusySince = null;
47
79
  function _resetClaudeCodeUpdaterState() {
48
80
  lastCheckAt = 0;
49
81
  multiInstallWarned = false;
50
82
  verifyMismatchState = null;
83
+ belowFloorBusySince = null;
51
84
  }
52
85
  function defaultPsSnapshot() {
53
86
  return execFileSync("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
@@ -138,6 +171,8 @@ async function maybeUpdateClaudeCode(deps) {
138
171
  const realPath = deps.realPath ?? realpathSync;
139
172
  const home = (deps.homeDir ?? homedir)();
140
173
  const { log } = deps;
174
+ const floor = deps.securityFloor ?? CLAUDE_CODE_SECURITY_FLOOR;
175
+ let forcedUnderLiveAgents = false;
141
176
  if (now - lastCheckAt < intervalMs()) {
142
177
  return { action: "throttled" };
143
178
  }
@@ -176,6 +211,13 @@ async function maybeUpdateClaudeCode(deps) {
176
211
  }
177
212
  target = resolved;
178
213
  }
214
+ const targetVsFloor = compareVersions(target, floor);
215
+ if (targetVsFloor !== null && targetVsFloor < 0) {
216
+ log(
217
+ `[claude-code-updater] target ${target} is below the security floor ${floor} (ENG-11641: CVE-2026-111497 + emSign root) \u2014 raising the target to the floor`
218
+ );
219
+ target = floor;
220
+ }
179
221
  let current = deps.currentVersion ?? null;
180
222
  if (!current) {
181
223
  try {
@@ -185,6 +227,8 @@ async function maybeUpdateClaudeCode(deps) {
185
227
  current = null;
186
228
  }
187
229
  }
230
+ const belowFloor = isBelowSecurityFloor(current, floor);
231
+ if (!belowFloor) belowFloorBusySince = null;
188
232
  if (current && current === target) {
189
233
  if (verifyMismatchState && verifyMismatchState.target === target) {
190
234
  verifyMismatchState = null;
@@ -192,8 +236,47 @@ async function maybeUpdateClaudeCode(deps) {
192
236
  return { action: "up_to_date", version: target };
193
237
  }
194
238
  if (hostHasLiveAgent(ps())) {
195
- log(`[claude-code-updater] host busy (live agent) \u2014 deferring update to ${target} to a later cycle`);
196
- return { action: "deferred_busy", target };
239
+ if (!belowFloor) {
240
+ log(`[claude-code-updater] host busy (live agent) \u2014 deferring update to ${target} to a later cycle`);
241
+ return { action: "deferred_busy", target };
242
+ }
243
+ if (belowFloorBusySince === null) belowFloorBusySince = now;
244
+ const waitedMs = now - belowFloorBusySince;
245
+ if (waitedMs < FORCED_UPDATE_HARD_DEADLINE_MS) {
246
+ lastCheckAt = now - intervalMs() + BELOW_FLOOR_RECHECK_MS;
247
+ log(
248
+ `[claude-code-updater] host busy (live agent) and ${current} is below the security floor ${floor} \u2014 deferring update to ${target}; will force it in ${Math.ceil((FORCED_UPDATE_HARD_DEADLINE_MS - waitedMs) / 6e4)}m if the npm install slot is the binary agents run`
249
+ );
250
+ return { action: "deferred_busy", target };
251
+ }
252
+ const slotPath = await resolveInstallTargetPath(run);
253
+ let slotVersion = null;
254
+ if (slotPath) {
255
+ try {
256
+ slotVersion = parseVersion((await run(slotPath, ["--version"])).stdout);
257
+ } catch {
258
+ slotVersion = null;
259
+ }
260
+ }
261
+ if (!slotPath) {
262
+ belowFloorBusySince = null;
263
+ log(
264
+ `[claude-code-updater] ERROR security_floor_blocked: deadline reached but \`npm prefix -g\` did not resolve \u2014 cannot prove an install would reach the binary agents run; not forcing an install under live agents`
265
+ );
266
+ return { action: "deferred_busy", target };
267
+ }
268
+ const slotIsTheRunningBinary = slotVersion !== null && isBelowSecurityFloor(slotVersion, floor) && slotVersion === current;
269
+ if (!slotIsTheRunningBinary) {
270
+ belowFloorBusySince = null;
271
+ log(
272
+ `[claude-code-updater] ERROR security_floor_blocked: PATH claude reports ${current} (below the security floor ${floor}) but the npm install slot ${slotPath} reports ${slotVersion ?? "no readable version"} \u2014 cannot show the install would reach the binary agents run (multi-install, ENG-5798); not forcing an install under live agents, investigate the host's claude install slots`
273
+ );
274
+ return { action: "deferred_busy", target };
275
+ }
276
+ log(
277
+ `[claude-code-updater] security floor deadline reached: ${current} < ${floor} and host still busy after ${Math.round(waitedMs / 6e4)}m \u2014 installing ${target} anyway (ENG-11641)`
278
+ );
279
+ forcedUnderLiveAgents = true;
197
280
  }
198
281
  const installTargetPath = await resolveInstallTargetPath(run);
199
282
  log(`[claude-code-updater] updating Claude Code ${current ?? "(none)"} \u2192 ${target}`);
@@ -207,6 +290,7 @@ async function maybeUpdateClaudeCode(deps) {
207
290
  installed = null;
208
291
  }
209
292
  if (installed !== target) {
293
+ belowFloorBusySince = null;
210
294
  log(
211
295
  `[claude-code-updater] post-install verify mismatch: expected ${target}, got ${installed ?? "unknown"} from ${verifyTarget}`
212
296
  );
@@ -226,22 +310,33 @@ async function maybeUpdateClaudeCode(deps) {
226
310
  return { action: "failed", reason: "verify_mismatch" };
227
311
  }
228
312
  verifyMismatchState = null;
313
+ belowFloorBusySince = null;
229
314
  log(`[claude-code-updater] updated Claude Code ${current ?? "(none)"} \u2192 ${installed}`);
315
+ if (forcedUnderLiveAgents) {
316
+ log(
317
+ `[claude-code-updater] WARNING installed ${installed} under live agents: running sessions keep the previous ${current ?? "unknown"} binary until they restart. framework_version now reports ${installed}, which does NOT mean the live sessions are patched`
318
+ );
319
+ }
230
320
  return { action: "updated", from: current, to: installed };
231
321
  } catch (err) {
322
+ if (forcedUnderLiveAgents) belowFloorBusySince = null;
232
323
  log(`[claude-code-updater] update attempt failed: ${err instanceof Error ? err.message : String(err)}`);
233
324
  return { action: "failed", reason: "exception" };
234
325
  }
235
326
  }
236
327
  export {
328
+ CLAUDE_CODE_SECURITY_FLOOR,
237
329
  WELL_KNOWN_CLAUDE_PATHS,
238
330
  _resetClaudeCodeUpdaterState,
331
+ compareVersions,
239
332
  hostHasLiveAgent,
333
+ isBelowSecurityFloor,
240
334
  isConcreteVersion,
241
335
  isNativeInstallPath,
242
336
  maybeUpdateClaudeCode,
243
337
  parseVersion,
244
338
  probeClaudeInstalls,
245
- readsAsNativeInstall
339
+ readsAsNativeInstall,
340
+ shouldRunClaudeCodeUpdater
246
341
  };
247
- //# sourceMappingURL=claude-code-updater-NEJCTT7S.js.map
342
+ //# sourceMappingURL=claude-code-updater-HTDCRIMM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/claude-code-updater.ts"],"sourcesContent":["// ENG-5672: idle-gated, server-pinned Claude Code auto-updater.\n// ENG-5798: multi-install drift handling — detect coexisting claude binaries,\n// short-circuit on native autoupdater hosts, verify against absolute\n// install path, hard-error after N consecutive verify mismatches.\n//\n// Hosts install @anthropic-ai/claude-code once at bootstrap (host-bootstrap.ts)\n// and never refresh it, so a long-lived host runs a stale Claude Code forever.\n// This module brings a host to its *desired* version, which the server pins via\n// /host/refresh (per-host override, else fleet default, else 'latest').\n//\n// Safety properties (this runs npm install -g on live prod hosts):\n// • Idle-gated — we never install while a `claude --name agt-*` agent process\n// is running on the host (a running process holds the old binary anyway;\n// replacing it under a live session is pointless and risky). Busy → defer\n// to a later cycle.\n// • No-op when already on target — a concrete target equal to the installed\n// version short-circuits; a dist-tag ('latest') is resolved to a concrete\n// version via `npm view` and compared, so we don't reinstall every cycle.\n// • Host-global throttle — at most one check per interval (default 6h), so the\n// manager's ~10-30s poll doesn't shell out to npm/ps constantly.\n// • Log-and-continue — every failure is logged and swallowed; an error here\n// must never break the manager poll cycle.\n// • Multi-install awareness (ENG-5798) — on hosts where Anthropic's native\n// autoupdater owns /usr/bin/claude, this updater short-circuits cleanly\n// instead of fighting it. On hosts where multiple binaries coexist (e.g. a\n// brew cask alongside the npm global), we log every path+version once so\n// drift is visible in manager.log without spamming every cycle.\n//\n// The *installed* version is reported separately as hosts.framework_version\n// (the claude-code framework adapter's getVersion() == `claude --version`), so\n// this module does not report it itself.\n\nimport { execFile, execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync, realpathSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { promisify } from 'node:util';\nimport { effectiveClaudeJsonPath } from './claude-config-location.js';\nimport { parsePsRows, CLAUDE_AGENT_PROCESS_PATTERN } from './orphan-channel-mcp-reaper.js';\nimport { FORCED_UPDATE_HARD_DEADLINE_MS } from '@augmented/core';\n\nconst execFileAsync = promisify(execFile);\n\nconst NPM_PKG = '@anthropic-ai/claude-code';\n\n/**\n * ENG-11641 follow-up: the minimum host Claude Code version, for security.\n *\n * The npm package is a native binary with its own bundled Bun, so patching\n * system Node does nothing for it. Versions before 2.1.308 bundle a Bun whose\n * Brotli decoder predates the CVE-2026-111497 (Node2Shell) fix, and 2.1.280 /\n * 2.1.296 also ship the compromised emSign ECC Root CA - G3 in their built-in\n * trust store (eMudhra intermediate key compromise, oss-security 2026-10-20).\n * #7108 fixed the agt-runtime IMAGE; it did nothing for the host's own global\n * install, which #7108 measured at 2.1.280 on 22 of 24 prod hosts. Those hosts\n * never converge because this updater only runs inside the maintenance window\n * AND only when no agent is live, and a working fleet is never idle.\n *\n * A host below this floor therefore:\n * • has its target raised to the floor when the pin/fleet default is lower\n * (a pin below a security floor IS the vulnerability, so it is overridden\n * and logged, not honoured);\n * • bypasses the maintenance window (see manager-worker's call site);\n * • still defers while an agent is live, for at most\n * FORCED_UPDATE_HARD_DEADLINE_MS (the same deadline the \"Update CLI now\"\n * forced update uses, ENG-8449), then installs under live agents ONLY IF\n * the npm install slot is provably the binary agents run (its version is\n * readable, below the floor and equal to PATH's). Otherwise — unresolvable\n * slot, unreadable slot, slot already fixed, or slot on a different version\n * (the ENG-5798 multi-install case) — it does NOT force, logs\n * `ERROR security_floor_blocked`, and the host stays below the floor until\n * an operator fixes its install slots. That is a known, alarmed limit, not\n * a guarantee of convergence.\n *\n * Raise this when a new security floor is set; never lower it.\n */\nexport const CLAUDE_CODE_SECURITY_FLOOR = '2.1.308';\n\n/** How soon to re-check a below-floor host that deferred because it was busy. */\nconst BELOW_FLOOR_RECHECK_MS = 60_000;\n\n/** Default host-global check interval. Overridable via env for tuning. */\nconst DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h\n\n/**\n * ENG-5798: After this many consecutive verify mismatches against the same\n * target version, emit a distinct one-shot error code that the\n * synthetic-probe / CloudWatch alarm pipeline can latch onto, instead of\n * silently re-failing forever. Overridable via env for testing.\n */\nconst DEFAULT_VERIFY_MISMATCH_THRESHOLD = 5;\n\nfunction intervalMs(): number {\n const raw = Number.parseInt(process.env['AGT_CLAUDE_UPDATE_INTERVAL_MS'] ?? '', 10);\n return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_INTERVAL_MS;\n}\n\nfunction verifyMismatchThreshold(): number {\n const raw = Number.parseInt(process.env['AGT_CLAUDE_UPDATE_VERIFY_THRESHOLD'] ?? '', 10);\n return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_VERIFY_MISMATCH_THRESHOLD;\n}\n\n/** Extract a version string (leading semver) from `claude --version` / npm output. */\nexport function parseVersion(output: string): string | null {\n const m = output.match(/\\d+\\.\\d+\\.\\d+[A-Za-z0-9.+-]*/);\n return m ? m[0] : null;\n}\n\n/** A target spec is \"concrete\" when it already names an exact version. */\nexport function isConcreteVersion(spec: string): boolean {\n return /^\\d+\\.\\d+\\.\\d+[A-Za-z0-9.+-]*$/.test(spec.trim());\n}\n\n/**\n * Compare two concrete versions by their numeric major.minor.patch core.\n * A prerelease (`1.2.3-beta.1`) sorts BEFORE its release (`1.2.3`), as in\n * semver. Returns null when either side does not parse, so callers can fail\n * closed rather than guess.\n */\nexport function compareVersions(a: string, b: string): number | null {\n const re = /^(\\d+)\\.(\\d+)\\.(\\d+)(-[0-9A-Za-z.-]+)?/;\n const ma = re.exec(a.trim());\n const mb = re.exec(b.trim());\n if (!ma || !mb) return null;\n for (let i = 1; i <= 3; i++) {\n const d = Number(ma[i]) - Number(mb[i]);\n if (d !== 0) return d < 0 ? -1 : 1;\n }\n const preA = ma[4] !== undefined;\n const preB = mb[4] !== undefined;\n if (preA === preB) return 0;\n return preA ? -1 : 1;\n}\n\n/**\n * Whether the manager should call the updater on this poll (ENG-11641\n * follow-up). Normally only inside the maintenance window; a host whose\n * installed Claude Code is below the security floor runs it regardless of the\n * window. No server target means \"change nothing\" (ENG-10620).\n */\nexport function shouldRunClaudeCodeUpdater(input: {\n desiredVersion: string | null | undefined;\n installedVersion: string | null | undefined;\n windowGate: 'proceed' | 'defer';\n floor?: string;\n}): boolean {\n if (!input.desiredVersion) return false;\n if (isBelowSecurityFloor(input.installedVersion, input.floor)) return true;\n return input.windowGate === 'proceed';\n}\n\n/**\n * True when `version` is a known version strictly below\n * {@link CLAUDE_CODE_SECURITY_FLOOR}. Unknown/unparseable → false: we only\n * escalate past the normal gates on a measured version, never on a guess.\n */\nexport function isBelowSecurityFloor(\n version: string | null | undefined,\n floor: string = CLAUDE_CODE_SECURITY_FLOOR,\n): boolean {\n if (!version) return false;\n const parsed = parseVersion(version);\n if (!parsed) return false;\n const cmp = compareVersions(parsed, floor);\n return cmp !== null && cmp < 0;\n}\n\n/**\n * Well-known absolute paths where a `claude` binary may live on managed hosts.\n * Probed alongside the PATH-resolved binary so we can see all install slots,\n * even ones not on the manager systemd unit's PATH.\n */\nexport const WELL_KNOWN_CLAUDE_PATHS: readonly string[] = [\n '/usr/bin/claude', // Anthropic native autoupdater (Linux deb/rpm)\n '/usr/local/bin/claude', // npm global on macOS Intel + Linux without nvm\n '/opt/homebrew/bin/claude', // Homebrew on macOS Apple Silicon\n '/home/linuxbrew/.linuxbrew/bin/claude', // Linuxbrew (used on agt-aws-* hosts)\n];\n\nexport interface ClaudeCodeUpdaterDeps {\n /** Server-pinned target: a concrete version or an npm dist-tag like 'latest'. */\n desiredVersion: string;\n /** Currently-installed version, if the caller already knows it (e.g. the\n * manager's cachedFrameworkVersion). When omitted we run `claude --version`. */\n currentVersion?: string | null;\n log: (msg: string) => void;\n /** Override \"now\" for tests. */\n now?: () => number;\n /** Run a command; injected in tests. Defaults to execFile. */\n runCommand?: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>;\n /** Raw `ps -eo pid,ppid,args` snapshot; injected in tests. */\n psSnapshot?: () => string;\n /** Sync existence probe for binary detection; injected in tests. */\n fileExists?: (path: string) => boolean;\n /** Read ~/.claude.json contents (raw); returns null when absent. Injected in tests. */\n readClaudeJson?: (path: string) => string | null;\n /** Resolve the user's home directory; injected in tests. */\n homeDir?: () => string;\n /** Resolve symlinks to a real path (throws when absent); injected in tests. */\n realPath?: (path: string) => string;\n /** Security floor override; defaults to CLAUDE_CODE_SECURITY_FLOOR. Injected in tests. */\n securityFloor?: string;\n}\n\nexport type UpdaterOutcome =\n | { action: 'throttled' }\n | { action: 'up_to_date'; version: string }\n | { action: 'deferred_busy'; target: string }\n | { action: 'skipped_native_install'; target: string }\n | { action: 'updated'; from: string | null; to: string }\n | { action: 'failed'; reason: string };\n\n// ── Host-global state (module-level; one updater per host process) ──────────\nlet lastCheckAt = 0;\n/** Per-process one-shot guard for the multi-install warn line. */\nlet multiInstallWarned = false;\n/**\n * Sticky counter for consecutive `post-install verify mismatch` against the\n * same target version. Reset on success, on target change, or after the\n * threshold error has been emitted (so we emit it exactly once per stuck run).\n */\nlet verifyMismatchState: { target: string; count: number; alarmed: boolean } | null = null;\n/**\n * When a below-floor host first deferred because it was busy. The\n * FORCED_UPDATE_HARD_DEADLINE_MS clock runs from here; cleared once the host\n * is at or above the floor. Process-local on purpose: a manager restart\n * restarts the clock, which costs at most one more deadline period.\n */\nlet belowFloorBusySince: number | null = null;\n\n/** Test helper — reset module state. Not for production use. */\nexport function _resetClaudeCodeUpdaterState(): void {\n lastCheckAt = 0;\n multiInstallWarned = false;\n verifyMismatchState = null;\n belowFloorBusySince = null;\n}\n\nfunction defaultPsSnapshot(): string {\n return execFileSync('ps', ['-eo', 'pid,ppid,args'], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });\n}\n\nfunction defaultReadClaudeJson(path: string): string | null {\n try {\n return readFileSync(path, 'utf-8');\n } catch {\n return null;\n }\n}\n\n/** True when at least one `claude --name agt-*` agent process is live. */\nexport function hostHasLiveAgent(psOutput: string): boolean {\n return parsePsRows(psOutput).some((r) => CLAUDE_AGENT_PROCESS_PATTERN.test(r.args));\n}\n\n/**\n * ENG-5798: returns true when ~/.claude.json reports `installMethod: \"native\"`,\n * meaning Anthropic's native autoupdater owns the on-disk binary. In that case\n * the brew/npm update path is actively harmful — it installs a second binary\n * the host's PATH never resolves to, and verify reads from yet a third.\n */\nexport function readsAsNativeInstall(raw: string | null): boolean {\n if (!raw) return false;\n try {\n const parsed = JSON.parse(raw) as { installMethod?: unknown };\n return parsed.installMethod === 'native';\n } catch {\n return false;\n }\n}\n\n/**\n * ENG-10620: true when `realPath` is a binary owned by Claude Code's native\n * installer, which keeps every version under `…/claude/versions/<semver>`\n * (e.g. `~/.local/share/claude/versions/2.1.274`, symlinked from\n * `~/.local/bin/claude`). An npm global resolves into\n * `…/node_modules/@anthropic-ai/claude-code/` instead.\n */\nexport function isNativeInstallPath(realPath: string): boolean {\n return /\\/claude\\/versions\\/\\d+\\.\\d+\\.\\d+[^/]*$/.test(realPath);\n}\n\n/**\n * ENG-10620: decide native ownership from the binary this host ACTUALLY runs,\n * not from `~/.claude.json`.\n *\n * `installMethod` is written by whichever `claude` last started, not by the\n * one the manager launches. On agt-aws-1 an operator shell's PATH resolved a\n * native `~/.local/bin/claude` while agents ran the npm global at\n * `/usr/bin/claude`; that one manual run flipped `installMethod` to `native`\n * and this updater stopped converging the host to its pin for good, while the\n * binary it would have updated was never native at all.\n *\n * Resolution mirrors how agent sessions pick their binary (resolveClaudeBinary:\n * CLAUDE_PATH, then PATH). The config key remains only as a fallback for a\n * host where no binary can be resolved.\n */\nasync function resolveNativeOwnership(\n run: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>,\n realPath: (path: string) => string,\n claudeJson: string | null,\n): Promise<{ native: boolean; binary: string | null; configSaysNative: boolean }> {\n const configSaysNative = readsAsNativeInstall(claudeJson);\n let resolved: string | null = null;\n const override = process.env['CLAUDE_PATH'];\n if (override) {\n resolved = override;\n } else {\n try {\n const { stdout } = await run('which', ['claude']);\n resolved = stdout.trim().split('\\n')[0]?.trim() || null;\n } catch {\n resolved = null;\n }\n }\n if (resolved) {\n try {\n const real = realPath(resolved);\n return { native: isNativeInstallPath(real), binary: real, configSaysNative };\n } catch {\n /* unresolvable — fall back to the config key */\n }\n }\n return { native: configSaysNative, binary: null, configSaysNative };\n}\n\n/**\n * Resolve the absolute filesystem path that `npm install -g <pkg>` will write\n * the `claude` binary to. Returns null when `npm prefix -g` fails or is empty,\n * in which case the caller should fall back to PATH-based verify.\n *\n * We deliberately use `npm prefix -g` rather than `which claude` because the\n * latter resolves through PATH, which on agt-aws-* hosts can land on\n * /usr/bin/claude (the native autoupdater slot) — exactly the misalignment\n * ENG-5798 is fixing.\n */\nasync function resolveInstallTargetPath(\n run: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>,\n): Promise<string | null> {\n try {\n const { stdout } = await run('npm', ['prefix', '-g']);\n const prefix = stdout.trim();\n if (!prefix) return null;\n return `${prefix}/bin/claude`;\n } catch {\n return null;\n }\n}\n\n/**\n * Enumerate every `claude` binary visible to the host: PATH-resolved + the\n * well-known absolute slots + `~/.claude/local/claude`. Returns one entry per\n * unique resolved path with the version each reports (or null if it failed to\n * execute). Used purely for the one-shot drift warn line.\n */\nexport async function probeClaudeInstalls(\n run: (cmd: string, args: string[]) => Promise<{ stdout: string; stderr: string }>,\n fileExists: (path: string) => boolean,\n home: string,\n): Promise<Array<{ path: string; version: string | null }>> {\n const candidates = new Set<string>(WELL_KNOWN_CLAUDE_PATHS);\n candidates.add(`${home}/.claude/local/claude`);\n // PATH-resolved. `which` is portable enough across the Linux + macOS hosts\n // we target; errors here just mean \"no claude on PATH\", which is fine.\n try {\n const { stdout } = await run('which', ['claude']);\n const p = stdout.trim().split('\\n')[0]?.trim();\n if (p) candidates.add(p);\n } catch {\n /* no-op */\n }\n\n const results: Array<{ path: string; version: string | null }> = [];\n for (const p of candidates) {\n if (!fileExists(p)) continue;\n let version: string | null = null;\n try {\n const { stdout } = await run(p, ['--version']);\n version = parseVersion(stdout);\n } catch {\n /* binary exists but failed to execute — record path with null version */\n }\n results.push({ path: p, version });\n }\n return results;\n}\n\n/**\n * Bring Claude Code to the desired version if the host is idle and not already\n * on target. Safe to call every manager poll — internally throttled. Returns an\n * outcome describing what happened (for logging/observability/tests).\n */\nexport async function maybeUpdateClaudeCode(deps: ClaudeCodeUpdaterDeps): Promise<UpdaterOutcome> {\n const now = deps.now ? deps.now() : Date.now();\n const run = deps.runCommand ?? ((cmd, args) => execFileAsync(cmd, args, { maxBuffer: 16 * 1024 * 1024 }));\n const ps = deps.psSnapshot ?? defaultPsSnapshot;\n const fileExists = deps.fileExists ?? existsSync;\n const readClaudeJson = deps.readClaudeJson ?? defaultReadClaudeJson;\n const realPath = deps.realPath ?? realpathSync;\n const home = (deps.homeDir ?? homedir)();\n const { log } = deps;\n const floor = deps.securityFloor ?? CLAUDE_CODE_SECURITY_FLOOR;\n let forcedUnderLiveAgents = false;\n\n // 1. Host-global throttle.\n if (now - lastCheckAt < intervalMs()) {\n return { action: 'throttled' };\n }\n lastCheckAt = now;\n\n try {\n const spec = (deps.desiredVersion || 'latest').trim();\n\n // 2. ENG-5798 multi-install drift detection. Emit one warn line per\n // process listing every claude binary + version we can see. This is\n // independent of the install decision below — even on hosts where we\n // proceed normally, surfacing drift is the difference between a 3-day\n // silent loop and a one-line lead.\n if (!multiInstallWarned) {\n const installs = await probeClaudeInstalls(run, fileExists, home);\n if (installs.length > 1) {\n const summary = installs\n .map((i) => `${i.path}=${i.version ?? 'unknown'}`)\n .join(', ');\n log(`[claude-code-updater] multiple claude installs detected: ${summary}`);\n }\n multiInstallWarned = true;\n }\n\n // 3. ENG-5798 native-install short-circuit. When Anthropic's native\n // autoupdater owns the host binary, the brew/npm update path is\n // actively harmful (it installs a second binary the manager's PATH\n // never resolves to, then verifies against yet a third). Defer.\n // ENG-10869: the path the AGENT actually reads, not the legacy one.\n // Once this host migrates, `~/.claude.json` is ARCHIVED, so a hardcoded\n // read here returns nothing forever and `configSaysNative` is permanently\n // false — silently re-opening the ENG-5798 harm this short-circuit exists\n // to prevent (brew/npm installing a second binary on a host Anthropic's\n // native autoupdater owns). The resolver falls back to the legacy file, so\n // an unmigrated host is unaffected.\n const claudeJsonPath = effectiveClaudeJsonPath(home);\n const ownership = await resolveNativeOwnership(run, realPath, readClaudeJson(claudeJsonPath));\n if (!ownership.native && ownership.configSaysNative) {\n log(\n `[claude-code-updater] ${claudeJsonPath} says installMethod=native but the binary this host runs (${ownership.binary}) is not a native install — ignoring it and converging to the pin`,\n );\n }\n if (ownership.native) {\n // One-shot info line per process — same throttle as the warn above, but\n // we re-use the throttle window to keep this from logging every poll.\n log('[claude-code-updater] native install detected, deferring to Anthropic autoupdater');\n // Reset any sticky verify-mismatch state — it can't apply once we stop\n // attempting installs.\n verifyMismatchState = null;\n return { action: 'skipped_native_install', target: spec };\n }\n\n // 4. Resolve the target to a concrete version (read-only). A concrete spec\n // is its own target; a dist-tag is resolved via `npm view`.\n let target: string;\n if (isConcreteVersion(spec)) {\n target = spec;\n } else {\n const { stdout } = await run('npm', ['view', `${NPM_PKG}@${spec}`, 'version']);\n const resolved = parseVersion(stdout);\n if (!resolved) {\n log(`[claude-code-updater] could not resolve ${NPM_PKG}@${spec} to a version; skipping`);\n return { action: 'failed', reason: 'unresolved_target' };\n }\n target = resolved;\n }\n\n // 4b. Security floor: never converge to a version below it. A pin (or a\n // stale fleet default) under the floor would otherwise short-circuit at\n // step 6 as \"up_to_date\" and keep the host vulnerable forever.\n const targetVsFloor = compareVersions(target, floor);\n if (targetVsFloor !== null && targetVsFloor < 0) {\n log(\n `[claude-code-updater] target ${target} is below the security floor ${floor} (ENG-11641: CVE-2026-111497 + emSign root) — raising the target to the floor`,\n );\n target = floor;\n }\n\n // 5. Determine the installed version (prefer the caller's cached value).\n let current = deps.currentVersion ?? null;\n if (!current) {\n try {\n const { stdout } = await run('claude', ['--version']);\n current = parseVersion(stdout);\n } catch {\n current = null; // claude missing/broken — treat as \"needs install\".\n }\n }\n\n const belowFloor = isBelowSecurityFloor(current, floor);\n if (!belowFloor) belowFloorBusySince = null;\n\n // 6. Already on target → nothing to do.\n if (current && current === target) {\n // Successful no-op — clear sticky verify mismatch state for this target.\n if (verifyMismatchState && verifyMismatchState.target === target) {\n verifyMismatchState = null;\n }\n return { action: 'up_to_date', version: target };\n }\n\n // 7. Idle gate — never replace the binary under a live agent session.\n // Exception: a host below the security floor defers for at most\n // FORCED_UPDATE_HARD_DEADLINE_MS, then installs under live agents if the\n // npm slot is provably the running binary (see the module header; the\n // same bounded-wait shape as the forced agt-cli update, ENG-8449).\n if (hostHasLiveAgent(ps())) {\n if (!belowFloor) {\n log(`[claude-code-updater] host busy (live agent) — deferring update to ${target} to a later cycle`);\n return { action: 'deferred_busy', target };\n }\n if (belowFloorBusySince === null) belowFloorBusySince = now;\n const waitedMs = now - belowFloorBusySince;\n if (waitedMs < FORCED_UPDATE_HARD_DEADLINE_MS) {\n // Re-check in ~1 min instead of the normal 6h throttle, so the deadline\n // is actually observed when it passes.\n lastCheckAt = now - intervalMs() + BELOW_FLOOR_RECHECK_MS;\n log(\n `[claude-code-updater] host busy (live agent) and ${current} is below the security floor ${floor} — deferring update to ${target}; will force it in ${Math.ceil((FORCED_UPDATE_HARD_DEADLINE_MS - waitedMs) / 60_000)}m if the npm install slot is the binary agents run`,\n );\n return { action: 'deferred_busy', target };\n }\n // Review 97810715f303 / ENG-5798: before overriding the idle gate, check\n // the slot npm actually writes. If it is ALREADY at or above the floor,\n // the below-floor reading came from a different binary on PATH (the\n // multi-install case), so an install here cannot fix anything. Forcing it\n // would re-run `npm install -g` under live agents every cycle while the\n // PATH binary stays put. Fall back to the normal idle-gated behaviour and\n // say why.\n const slotPath = await resolveInstallTargetPath(run);\n let slotVersion: string | null = null;\n if (slotPath) {\n try {\n slotVersion = parseVersion((await run(slotPath, ['--version'])).stdout);\n } catch {\n slotVersion = null;\n }\n }\n // Review fe4300a0793c: an unresolvable slot means we cannot prove an\n // install would help either, so it gets the same fall-back.\n if (!slotPath) {\n belowFloorBusySince = null;\n log(\n `[claude-code-updater] ERROR security_floor_blocked: deadline reached but \\`npm prefix -g\\` did not resolve — cannot prove an install would reach the binary agents run; not forcing an install under live agents`,\n );\n return { action: 'deferred_busy', target };\n }\n // Review 0a3b967ea569: only force when there is positive evidence that\n // the slot npm writes IS the binary agents run and IS below the floor:\n // its version is readable, below the floor, and equal to what PATH\n // reports. An empty/broken slot (version unreadable), a slot already at\n // or above the floor, or a slot on a different version than PATH all mean\n // an install cannot be shown to help, so fall back to the idle gate.\n const slotIsTheRunningBinary =\n slotVersion !== null && isBelowSecurityFloor(slotVersion, floor) && slotVersion === current;\n if (!slotIsTheRunningBinary) {\n belowFloorBusySince = null;\n log(\n `[claude-code-updater] ERROR security_floor_blocked: PATH claude reports ${current} (below the security floor ${floor}) but the npm install slot ${slotPath} reports ${slotVersion ?? 'no readable version'} — cannot show the install would reach the binary agents run (multi-install, ENG-5798); not forcing an install under live agents, investigate the host's claude install slots`,\n );\n return { action: 'deferred_busy', target };\n }\n log(\n `[claude-code-updater] security floor deadline reached: ${current} < ${floor} and host still busy after ${Math.round(waitedMs / 60_000)}m — installing ${target} anyway (ENG-11641)`,\n );\n forcedUnderLiveAgents = true;\n }\n\n // 8. Install the target, then verify against the *absolute* path of the\n // binary npm just wrote — not via PATH. PATH resolution is the original\n // ENG-5798 bug: on hosts with multiple claude installs, PATH lands on a\n // different binary than the one npm overwrites, so verify never agrees.\n const installTargetPath = await resolveInstallTargetPath(run);\n log(`[claude-code-updater] updating Claude Code ${current ?? '(none)'} → ${target}`);\n await run('npm', ['install', '-g', `${NPM_PKG}@${target}`]);\n\n // Prefer the absolute install target; fall back to PATH only when\n // `npm prefix -g` failed (in which case verify is best-effort).\n const verifyTarget = installTargetPath ?? 'claude';\n let installed: string | null = null;\n try {\n const { stdout } = await run(verifyTarget, ['--version']);\n installed = parseVersion(stdout);\n } catch {\n installed = null;\n }\n\n if (installed !== target) {\n // Review fe4300a0793c: a forced install that did not verify must not keep\n // the old deadline, or every later cycle would force again immediately.\n belowFloorBusySince = null;\n log(\n `[claude-code-updater] post-install verify mismatch: expected ${target}, got ${installed ?? 'unknown'} from ${verifyTarget}`,\n );\n\n // ENG-5798: hard-error after N consecutive mismatches against the same\n // target. Reset the counter when the target changes — a new pin is a\n // new event, not a continuation of the old stuck run.\n if (!verifyMismatchState || verifyMismatchState.target !== target) {\n verifyMismatchState = { target, count: 1, alarmed: false };\n } else {\n verifyMismatchState.count += 1;\n }\n\n const threshold = verifyMismatchThreshold();\n if (verifyMismatchState.count >= threshold && !verifyMismatchState.alarmed) {\n verifyMismatchState.alarmed = true;\n log(\n `[claude-code-updater] ERROR verify_mismatch_stuck: ${verifyMismatchState.count} consecutive verify failures for target ${target} (verify path: ${verifyTarget}) — install is not taking effect, investigate host install slots`,\n );\n return { action: 'failed', reason: 'verify_mismatch_threshold' };\n }\n return { action: 'failed', reason: 'verify_mismatch' };\n }\n\n // Successful install — clear sticky state. Also end any security-floor wait\n // episode, so a later below-floor reading starts a fresh deadline instead of\n // inheriting this one's (review 97810715f303).\n verifyMismatchState = null;\n belowFloorBusySince = null;\n log(`[claude-code-updater] updated Claude Code ${current ?? '(none)'} → ${installed}`);\n if (forcedUnderLiveAgents) {\n // Review 8f1c08ba3c77: from here `claude --version` (and so the heartbeat's\n // framework_version) reads the new version, but the live sessions that held\n // the idle gate still execute the OLD binary until they restart. Say so\n // loudly, so the host does not read as remediated when it is not. The\n // recycle of those sessions is runtime-image-recycle's job (#7134, host\n // mode: /proc/<pid>/exe reads \"(deleted)\").\n log(\n `[claude-code-updater] WARNING installed ${installed} under live agents: running sessions keep the previous ${current ?? 'unknown'} binary until they restart. framework_version now reports ${installed}, which does NOT mean the live sessions are patched`,\n );\n }\n return { action: 'updated', from: current, to: installed };\n } catch (err) {\n // Review f540008e5f47: a forced install that THREW (npm non-zero exit,\n // EACCES, registry/network error) is the commonest failed install. Like a\n // failed verify, it must not keep the old deadline.\n if (forcedUnderLiveAgents) belowFloorBusySince = null;\n // Log-and-continue: an updater failure must never break the manager tick.\n log(`[claude-code-updater] update attempt failed: ${err instanceof Error ? err.message : String(err)}`);\n return { action: 'failed', reason: 'exception' };\n }\n}\n"],"mappings":"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","names":[]}
@@ -269,7 +269,7 @@ async function spawnPairSession(session) {
269
269
  return { ok: true };
270
270
  } catch {
271
271
  }
272
- const { resolveClaudeBinary } = await import("./persistent-session-VNJDG3VN.js");
272
+ const { resolveClaudeBinary } = await import("./persistent-session-JKD36BCM.js");
273
273
  const claudeBin = resolveClaudeBinary();
274
274
  const pairEnv = {
275
275
  ...process.env,
@@ -739,4 +739,4 @@ export {
739
739
  startClaudePair,
740
740
  submitClaudePairCode
741
741
  };
742
- //# sourceMappingURL=claude-pair-runtime-A3OWKP3K.js.map
742
+ //# sourceMappingURL=claude-pair-runtime-VEDZCIJR.js.map