@integrity-labs/agt-cli 0.26.1 → 0.26.2-eng5706.1

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.
@@ -0,0 +1,96 @@
1
+ // src/lib/orphan-channel-mcp-reaper.ts
2
+ import { execFileSync } from "child_process";
3
+ var CHANNEL_MCP_PATTERN = /\.augmented\/_mcp\/(telegram-channel|slack-channel|direct-chat-channel)\.js/;
4
+ var CLAUDE_AGENT_PROCESS_PATTERN = /\bclaude\b.*--name\s+agt-[a-z0-9-]+/;
5
+ function parsePsRows(psOutput) {
6
+ const lines = psOutput.split(/\r?\n/);
7
+ const rows = [];
8
+ for (const raw of lines) {
9
+ const line = raw.trim();
10
+ if (!line) continue;
11
+ const match = line.match(/^(\d+)\s+(\d+)\s+(.*)$/);
12
+ if (!match) continue;
13
+ const pid = Number.parseInt(match[1], 10);
14
+ const ppid = Number.parseInt(match[2], 10);
15
+ if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;
16
+ rows.push({ pid, ppid, args: match[3] ?? "" });
17
+ }
18
+ return rows;
19
+ }
20
+ function findOrphanChannelMcps(rows, opts) {
21
+ const maxDepth = opts?.maxDepth ?? 8;
22
+ const byPid = new Map(rows.map((r) => [r.pid, r]));
23
+ const orphans = [];
24
+ for (const row of rows) {
25
+ if (!CHANNEL_MCP_PATTERN.test(row.args)) continue;
26
+ let cur = byPid.get(row.ppid);
27
+ let foundLiveClaude = false;
28
+ for (let depth = 0; depth < maxDepth && cur; depth++) {
29
+ if (CLAUDE_AGENT_PROCESS_PATTERN.test(cur.args)) {
30
+ foundLiveClaude = true;
31
+ break;
32
+ }
33
+ if (cur.pid === 1) break;
34
+ cur = byPid.get(cur.ppid);
35
+ }
36
+ if (!foundLiveClaude) orphans.push(row.pid);
37
+ }
38
+ return orphans;
39
+ }
40
+ function reapOrphanChannelMcps(args) {
41
+ const { log, graceMs = 5e3 } = args;
42
+ const runPs = args.runPs ?? (() => execFileSync("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", timeout: 5e3 }));
43
+ const killProcess = args.killProcess ?? ((pid, signal) => {
44
+ try {
45
+ process.kill(pid, signal);
46
+ } catch {
47
+ }
48
+ });
49
+ const isAlive = args.isAlive ?? ((pid) => {
50
+ try {
51
+ process.kill(pid, 0);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ });
57
+ let psOutput;
58
+ try {
59
+ psOutput = runPs();
60
+ } catch (err) {
61
+ log(`[orphan-reaper] ps invocation failed: ${err.message} \u2014 skipping reap`);
62
+ return [];
63
+ }
64
+ const rows = parsePsRows(psOutput);
65
+ const orphans = findOrphanChannelMcps(rows);
66
+ if (orphans.length === 0) return [];
67
+ const byPid = new Map(rows.map((r) => [r.pid, r]));
68
+ const describeOrphan = (pid) => {
69
+ const scriptMatch = byPid.get(pid)?.args.match(/([^\s/]+\.js)(?:\s|$)/);
70
+ return scriptMatch ? `${scriptMatch[1]} (pid ${pid})` : `pid ${pid}`;
71
+ };
72
+ log(`[orphan-reaper] sending SIGTERM to ${orphans.length} orphan channel-MCP(s): ${orphans.map(describeOrphan).join(", ")}`);
73
+ for (const pid of orphans) {
74
+ killProcess(pid, "SIGTERM");
75
+ }
76
+ setTimeout(() => {
77
+ try {
78
+ const stragglers = orphans.filter(isAlive);
79
+ if (stragglers.length === 0) return;
80
+ log(`[orphan-reaper] ${stragglers.length} orphan(s) survived SIGTERM; sending SIGKILL: ${stragglers.map(describeOrphan).join(", ")}`);
81
+ for (const pid of stragglers) {
82
+ killProcess(pid, "SIGKILL");
83
+ }
84
+ } catch (err) {
85
+ log(`[orphan-reaper] error in SIGKILL pass: ${err.message}`);
86
+ }
87
+ }, graceMs).unref();
88
+ return orphans;
89
+ }
90
+
91
+ export {
92
+ CLAUDE_AGENT_PROCESS_PATTERN,
93
+ parsePsRows,
94
+ reapOrphanChannelMcps
95
+ };
96
+ //# sourceMappingURL=chunk-XWVM4KPK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/orphan-channel-mcp-reaper.ts"],"sourcesContent":["// 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"],"mappings":"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","names":[]}
@@ -0,0 +1,101 @@
1
+ import {
2
+ CLAUDE_AGENT_PROCESS_PATTERN,
3
+ parsePsRows
4
+ } from "./chunk-XWVM4KPK.js";
5
+
6
+ // src/lib/claude-code-updater.ts
7
+ import { execFile, execFileSync } from "child_process";
8
+ import { promisify } from "util";
9
+ var execFileAsync = promisify(execFile);
10
+ var NPM_PKG = "@anthropic-ai/claude-code";
11
+ var DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1e3;
12
+ function intervalMs() {
13
+ const raw = Number.parseInt(process.env["AGT_CLAUDE_UPDATE_INTERVAL_MS"] ?? "", 10);
14
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_INTERVAL_MS;
15
+ }
16
+ function parseVersion(output) {
17
+ const m = output.match(/\d+\.\d+\.\d+[A-Za-z0-9.+-]*/);
18
+ return m ? m[0] : null;
19
+ }
20
+ function isConcreteVersion(spec) {
21
+ return /^\d+\.\d+\.\d+[A-Za-z0-9.+-]*$/.test(spec.trim());
22
+ }
23
+ var lastCheckAt = 0;
24
+ function _resetClaudeCodeUpdaterState() {
25
+ lastCheckAt = 0;
26
+ }
27
+ function defaultPsSnapshot() {
28
+ return execFileSync("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
29
+ }
30
+ function hostHasLiveAgent(psOutput) {
31
+ return parsePsRows(psOutput).some((r) => CLAUDE_AGENT_PROCESS_PATTERN.test(r.args));
32
+ }
33
+ async function maybeUpdateClaudeCode(deps) {
34
+ const now = deps.now ? deps.now() : Date.now();
35
+ const run = deps.runCommand ?? ((cmd, args) => execFileAsync(cmd, args, { maxBuffer: 16 * 1024 * 1024 }));
36
+ const ps = deps.psSnapshot ?? defaultPsSnapshot;
37
+ const { log } = deps;
38
+ if (now - lastCheckAt < intervalMs()) {
39
+ return { action: "throttled" };
40
+ }
41
+ lastCheckAt = now;
42
+ try {
43
+ const spec = (deps.desiredVersion || "latest").trim();
44
+ let target;
45
+ if (isConcreteVersion(spec)) {
46
+ target = spec;
47
+ } else {
48
+ const { stdout } = await run("npm", ["view", `${NPM_PKG}@${spec}`, "version"]);
49
+ const resolved = parseVersion(stdout);
50
+ if (!resolved) {
51
+ log(`[claude-code-updater] could not resolve ${NPM_PKG}@${spec} to a version; skipping`);
52
+ return { action: "failed", reason: "unresolved_target" };
53
+ }
54
+ target = resolved;
55
+ }
56
+ let current = deps.currentVersion ?? null;
57
+ if (!current) {
58
+ try {
59
+ const { stdout } = await run("claude", ["--version"]);
60
+ current = parseVersion(stdout);
61
+ } catch {
62
+ current = null;
63
+ }
64
+ }
65
+ if (current && current === target) {
66
+ return { action: "up_to_date", version: target };
67
+ }
68
+ if (hostHasLiveAgent(ps())) {
69
+ log(`[claude-code-updater] host busy (live agent) \u2014 deferring update to ${target} to a later cycle`);
70
+ return { action: "deferred_busy", target };
71
+ }
72
+ log(`[claude-code-updater] updating Claude Code ${current ?? "(none)"} \u2192 ${target}`);
73
+ await run("npm", ["install", "-g", `${NPM_PKG}@${target}`]);
74
+ let installed = null;
75
+ try {
76
+ const { stdout } = await run("claude", ["--version"]);
77
+ installed = parseVersion(stdout);
78
+ } catch {
79
+ installed = null;
80
+ }
81
+ if (installed !== target) {
82
+ log(
83
+ `[claude-code-updater] post-install verify mismatch: expected ${target}, got ${installed ?? "unknown"}`
84
+ );
85
+ return { action: "failed", reason: "verify_mismatch" };
86
+ }
87
+ log(`[claude-code-updater] updated Claude Code ${current ?? "(none)"} \u2192 ${installed}`);
88
+ return { action: "updated", from: current, to: installed };
89
+ } catch (err) {
90
+ log(`[claude-code-updater] update attempt failed: ${err instanceof Error ? err.message : String(err)}`);
91
+ return { action: "failed", reason: "exception" };
92
+ }
93
+ }
94
+ export {
95
+ _resetClaudeCodeUpdaterState,
96
+ hostHasLiveAgent,
97
+ isConcreteVersion,
98
+ maybeUpdateClaudeCode,
99
+ parseVersion
100
+ };
101
+ //# sourceMappingURL=claude-code-updater-7BGQ6JCV.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//\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//\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 { promisify } from 'node:util';\nimport { parsePsRows, CLAUDE_AGENT_PROCESS_PATTERN } from './orphan-channel-mcp-reaper.js';\n\nconst execFileAsync = promisify(execFile);\n\nconst NPM_PKG = '@anthropic-ai/claude-code';\n\n/** Default host-global check interval. Overridable via env for tuning. */\nconst DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h\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\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\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}\n\nexport type UpdaterOutcome =\n | { action: 'throttled' }\n | { action: 'up_to_date'; version: string }\n | { action: 'deferred_busy'; target: string }\n | { action: 'updated'; from: string | null; to: string }\n | { action: 'failed'; reason: string };\n\n// ── Host-global throttle state (module-level; one updater per host process) ──\nlet lastCheckAt = 0;\n\n/** Test helper — reset module state. Not for production use. */\nexport function _resetClaudeCodeUpdaterState(): void {\n lastCheckAt = 0;\n}\n\nfunction defaultPsSnapshot(): string {\n return execFileSync('ps', ['-eo', 'pid,ppid,args'], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });\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 * 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 { log } = deps;\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. 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 // 3. 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 // 4. Already on target → nothing to do.\n if (current && current === target) {\n return { action: 'up_to_date', version: target };\n }\n\n // 5. Idle gate — never replace the binary under a live agent session.\n if (hostHasLiveAgent(ps())) {\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\n // 6. Install the target, then verify.\n log(`[claude-code-updater] updating Claude Code ${current ?? '(none)'} → ${target}`);\n await run('npm', ['install', '-g', `${NPM_PKG}@${target}`]);\n\n let installed: string | null = null;\n try {\n const { stdout } = await run('claude', ['--version']);\n installed = parseVersion(stdout);\n } catch {\n installed = null;\n }\n\n if (installed !== target) {\n log(\n `[claude-code-updater] post-install verify mismatch: expected ${target}, got ${installed ?? 'unknown'}`,\n );\n return { action: 'failed', reason: 'verify_mismatch' };\n }\n\n log(`[claude-code-updater] updated Claude Code ${current ?? '(none)'} → ${installed}`);\n return { action: 'updated', from: current, to: installed };\n } catch (err) {\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":[]}
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-E5EZ32CI.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-K4X76LLB.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  const pairEnv = {
106
106
  ...process.env,
@@ -373,4 +373,4 @@ export {
373
373
  startClaudePair,
374
374
  submitClaudePairCode
375
375
  };
376
- //# sourceMappingURL=claude-pair-runtime-A35NZYDN.js.map
376
+ //# sourceMappingURL=claude-pair-runtime-2NNAUBP3.js.map