@integrity-labs/agt-cli 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agt.js +13 -4
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-AFUG4KD3.js → chunk-EG5D3KUV.js} +318 -41
- package/dist/chunk-EG5D3KUV.js.map +1 -0
- package/dist/{chunk-LU6L2J32.js → chunk-MEJGM5RV.js} +207 -15
- package/dist/chunk-MEJGM5RV.js.map +1 -0
- package/dist/{claude-pair-runtime-GS6AOYHS.js → claude-pair-runtime-Q7PNH3ZK.js} +41 -2
- package/dist/claude-pair-runtime-Q7PNH3ZK.js.map +1 -0
- package/dist/lib/manager-worker.js +163 -19
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/{persistent-session-VRS3MFQ3.js → persistent-session-YEUFJMWF.js} +8 -2
- package/package.json +1 -1
- package/dist/chunk-AFUG4KD3.js.map +0 -1
- package/dist/chunk-LU6L2J32.js.map +0 -1
- package/dist/claude-pair-runtime-GS6AOYHS.js.map +0 -1
- /package/dist/{persistent-session-VRS3MFQ3.js.map → persistent-session-YEUFJMWF.js.map} +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// src/lib/claude-pair-runtime.ts
|
|
2
2
|
import { execFile } from "child_process";
|
|
3
3
|
import { promisify } from "util";
|
|
4
|
+
import { existsSync, statSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
4
7
|
|
|
5
8
|
// src/lib/claude-pair-parser.ts
|
|
6
9
|
var ANSI_ESC = String.fromCharCode(27);
|
|
@@ -97,7 +100,7 @@ async function spawnPairSession(session) {
|
|
|
97
100
|
return { ok: true };
|
|
98
101
|
} catch {
|
|
99
102
|
}
|
|
100
|
-
const { resolveClaudeBinary } = await import("./persistent-session-
|
|
103
|
+
const { resolveClaudeBinary } = await import("./persistent-session-YEUFJMWF.js");
|
|
101
104
|
const claudeBin = resolveClaudeBinary();
|
|
102
105
|
try {
|
|
103
106
|
await execFileAsync("tmux", [
|
|
@@ -128,6 +131,41 @@ async function spawnPairSession(session) {
|
|
|
128
131
|
}
|
|
129
132
|
return { ok: true };
|
|
130
133
|
}
|
|
134
|
+
async function finalizeClaudePairOnboarding(session, log, opts = {}) {
|
|
135
|
+
const maxIterations = opts.maxIterations ?? 10;
|
|
136
|
+
const intervalMs = opts.intervalMs ?? 1500;
|
|
137
|
+
const claudeJsonPath = opts.claudeJsonPath ?? join(homedir(), ".claude.json");
|
|
138
|
+
const initialMtime = existsSync(claudeJsonPath) ? statSync(claudeJsonPath).mtimeMs : 0;
|
|
139
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
140
|
+
await sleep(intervalMs);
|
|
141
|
+
let pane;
|
|
142
|
+
try {
|
|
143
|
+
pane = await capturePane(session);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const classified = classifyTmuxError(err);
|
|
146
|
+
log(`[claude-pair] finalize: capture-pane failed (${classified.kind}); aborting onboarding flow`);
|
|
147
|
+
return { finalized: false, iterations: i };
|
|
148
|
+
}
|
|
149
|
+
if (pane.includes("Login successful") || pane.includes("Logged in as") || /Press Enter to continue/i.test(pane) || pane.includes("Security notes")) {
|
|
150
|
+
try {
|
|
151
|
+
await sendKeys(session, "C-m");
|
|
152
|
+
} catch {
|
|
153
|
+
log("[claude-pair] finalize: send-keys failed; aborting onboarding flow");
|
|
154
|
+
return { finalized: false, iterations: i };
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (existsSync(claudeJsonPath)) {
|
|
159
|
+
const mtime = statSync(claudeJsonPath).mtimeMs;
|
|
160
|
+
if (mtime > initialMtime) {
|
|
161
|
+
log(`[claude-pair] finalize: ~/.claude.json updated (after ${i + 1} dialog dismissal(s))`);
|
|
162
|
+
return { finalized: true, iterations: i + 1 };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
log(`[claude-pair] finalize: reached ${maxIterations} iterations without ~/.claude.json being updated`);
|
|
167
|
+
return { finalized: false, iterations: maxIterations };
|
|
168
|
+
}
|
|
131
169
|
async function killPairSession(session) {
|
|
132
170
|
try {
|
|
133
171
|
await execFileAsync("tmux", ["kill-session", "-t", session]);
|
|
@@ -311,6 +349,7 @@ async function getClaudePairStatus(session) {
|
|
|
311
349
|
return { kind: "idle" };
|
|
312
350
|
}
|
|
313
351
|
export {
|
|
352
|
+
finalizeClaudePairOnboarding,
|
|
314
353
|
getClaudePairStatus,
|
|
315
354
|
killPairSession,
|
|
316
355
|
pairTmuxSession,
|
|
@@ -318,4 +357,4 @@ export {
|
|
|
318
357
|
startClaudePair,
|
|
319
358
|
submitClaudePairCode
|
|
320
359
|
};
|
|
321
|
-
//# sourceMappingURL=claude-pair-runtime-
|
|
360
|
+
//# sourceMappingURL=claude-pair-runtime-Q7PNH3ZK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/claude-pair-runtime.ts","../src/lib/claude-pair-parser.ts"],"sourcesContent":["/**\n * ENG-4580: manager-side runtime for the Claude Code OAuth pairing flow.\n *\n * These functions own the actual tmux dance — sending `/login`,\n * polling the pane until Claude Code prints the OAuth URL, sending\n * the auth code, and detecting success/failure. They are the\n * counterpart to the pure parser in `claude-pair-parser.ts`.\n *\n * The API surface in ENG-4581 will wrap these — they don't include\n * any HTTP / DB code so the unit tests can target the parser layer\n * without spinning up a fake API. Errors are classified into the\n * `SessionError` shape so the API can translate them into structured\n * 4xx responses (e.g. `session_missing`).\n *\n * Architectural note: tmux capture-pane on a session that doesn't\n * exist exits non-zero with `can't find session`. Same goes for tmux\n * not being installed. classifyTmuxError covers both.\n */\n\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { existsSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\n\nimport {\n classifyTmuxError,\n detectAuthOutcome,\n extractOAuthUrl,\n isUrlPromptReady,\n type AuthOutcome,\n type SessionError,\n} from './claude-pair-parser.js';\n\nconst execFileAsync = promisify(execFile);\n\n// Pair-scoped tmux session name. The pairing flow runs inside a\n// throwaway `claude` instance — never inside an agent's persistent\n// session — so first-time auth on a fresh host works (no chicken-and-\n// egg) and re-auth doesn't disrupt an in-flight agent conversation.\nexport function pairTmuxSession(pairId: string): string {\n return `agt-pair-${pairId.slice(0, 12)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Low-level helpers\n// ---------------------------------------------------------------------------\n\ninterface CapturePaneOpts {\n /** How many lines of scrollback to include (negative = lines back). Default -200. */\n scrollback?: number;\n}\n\nasync function capturePane(session: string, opts: CapturePaneOpts = {}): Promise<string> {\n const scrollback = opts.scrollback ?? -200;\n const { stdout } = await execFileAsync('tmux', [\n 'capture-pane',\n '-t',\n session,\n '-p',\n '-S',\n String(scrollback),\n ]);\n return stdout;\n}\n\nasync function sendKeys(session: string, ...keys: string[]): Promise<void> {\n await execFileAsync('tmux', ['send-keys', '-t', session, ...keys]);\n}\n\nasync function sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n// ---------------------------------------------------------------------------\n// Pair-scoped tmux session lifecycle\n// ---------------------------------------------------------------------------\n\n/**\n * Spawn a throwaway tmux session running `claude` for the pairing flow.\n * Idempotent — if the session already exists, returns success silently.\n * Caller is responsible for `killPairSession` once the pair reaches a\n * terminal state.\n *\n * Uses an absolute path to the claude binary so we don't depend on the\n * inherited PATH (the manager runs with cloud-init's minimal env on\n * EC2). resolveClaudeBinary checks CLAUDE_PATH, then `which`, then\n * canonical Linux/macOS Homebrew install dirs.\n */\nexport async function spawnPairSession(session: string): Promise<{ ok: true } | { ok: false; error: SessionError }> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n return { ok: true };\n } catch {\n // session doesn't exist yet — fall through to create it\n }\n\n const { resolveClaudeBinary } = await import('./persistent-session.js');\n const claudeBin = resolveClaudeBinary();\n\n try {\n // -x 240 -y 50 — wide enough that Claude Code's OAuth URL (~350\n // chars after PKCE expansion) doesn't soft-wrap. The dewrap pass in\n // extractOAuthUrl handles any leftover wrapping, but giving the URL\n // a single line in the first place is more robust against future\n // claude UI changes.\n await execFileAsync('tmux', [\n 'new-session',\n '-d',\n '-x',\n '240',\n '-y',\n '50',\n '-s',\n session,\n claudeBin,\n ]);\n } catch (err) {\n return { ok: false, error: classifyTmuxError(err) };\n }\n\n // `tmux new-session` returns 0 even if the command exited immediately\n // (binary missing, claude crashed, etc.) — the session is created and\n // then torn down. Without this verification we'd propagate a generic\n // `session_missing` to the operator on the next has-session call,\n // hiding the real failure. Sleep briefly to give claude a beat to\n // crash-or-stay-alive, then check.\n await sleep(500);\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n } catch {\n return {\n ok: false,\n error: {\n kind: 'unknown',\n message: `claude exited immediately after launch (binary at ${claudeBin}). Run \\`${claudeBin}\\` manually on the host to see why — likely missing TTY, missing HOME, or a startup error.`,\n },\n };\n }\n return { ok: true };\n}\n\n/**\n * ENG-4633: drive Claude Code through its post-OAuth dialogs so it\n * persists `~/.claude.json` (the device-state + onboarding-complete\n * file) before we tear down the pair tmux session.\n *\n * Without this step, the pair flow leaves the host with only\n * `~/.claude/.credentials.json` written. On the next agent launch,\n * Claude Code interactive mode sees no `~/.claude.json` and falls\n * back to the login picker — even though the OAuth tokens are\n * sitting right there. This was the root cause of the 2026-05-01\n * prod scout outage.\n *\n * After successful code submission, claude shows in sequence:\n * 1. \"Logged in as <email> / Login successful. Press Enter to continue…\"\n * 2. \"Security notes / Press Enter to continue…\"\n * 3. (depending on cwd) trust-folder prompt and/or bypass-permissions\n * warning. The pair tmux session has neither a project dir nor\n * `--dangerously-skip-permissions`, so we usually only see the\n * first two.\n *\n * For each iteration: capture the pane, match a known prompt, send\n * Enter, repeat. Bail when `~/.claude.json`'s mtime advances past\n * the snapshot we took at entry — proxy for \"claude has rewritten\n * the file with post-login state\". The actual contents (e.g.\n * `hasCompletedOnboarding: true`) are not parsed here; we trust\n * claude to write a coherent file once it gets the chance, and\n * mtime-only is enough to gate the success log line. Returns\n * `finalized` so the caller can warn if onboarding never flushed —\n * the pair is still considered successful (OAuth tokens are valid\n * regardless), the worst-case fallout is a one-time login picker\n * on the next agent launch.\n */\nexport async function finalizeClaudePairOnboarding(\n session: string,\n log: (msg: string) => void,\n opts: { maxIterations?: number; intervalMs?: number; claudeJsonPath?: string } = {},\n): Promise<{ finalized: boolean; iterations: number }> {\n const maxIterations = opts.maxIterations ?? 10;\n const intervalMs = opts.intervalMs ?? 1500;\n const claudeJsonPath = opts.claudeJsonPath ?? join(homedir(), '.claude.json');\n\n // Snapshot the file's existence + mtime so we can detect the moment\n // claude actually writes it. We can't simply check \"does the file\n // exist\" because a stale file from a prior pair attempt would\n // satisfy that on the first iteration.\n const initialMtime = existsSync(claudeJsonPath)\n ? statSync(claudeJsonPath).mtimeMs\n : 0;\n\n for (let i = 0; i < maxIterations; i++) {\n await sleep(intervalMs);\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n // Session likely dead. Reading it back as failure here is fine —\n // the upstream caller already considered the pair successful, so\n // we report finalized=false and let it decide whether to fail\n // soft.\n const classified = classifyTmuxError(err);\n log(`[claude-pair] finalize: capture-pane failed (${classified.kind}); aborting onboarding flow`);\n return { finalized: false, iterations: i };\n }\n\n // Pattern-match the post-OAuth dialogs claude shows in succession.\n // Keep these matchers loose — claude's wording shifts between\n // versions, and a missed match just means we send Enter on the\n // generic \"Press Enter to continue\" branch below.\n if (\n pane.includes('Login successful') ||\n pane.includes('Logged in as') ||\n /Press Enter to continue/i.test(pane) ||\n pane.includes('Security notes')\n ) {\n try {\n await sendKeys(session, 'C-m');\n } catch {\n // sendKeys failure → session dead. Same fallthrough as above.\n log('[claude-pair] finalize: send-keys failed; aborting onboarding flow');\n return { finalized: false, iterations: i };\n }\n continue;\n }\n\n // Has claude rewritten ~/.claude.json since we entered this\n // function? mtime advance is our proxy for \"post-login state has\n // been flushed to disk\" — we don't crack the JSON open to verify\n // hasCompletedOnboarding because claude's exact write timing is\n // version-dependent and we'd be guessing about which key to look\n // at. Mtime-only is good enough for the smoke contract.\n if (existsSync(claudeJsonPath)) {\n const mtime = statSync(claudeJsonPath).mtimeMs;\n if (mtime > initialMtime) {\n log(`[claude-pair] finalize: ~/.claude.json updated (after ${i + 1} dialog dismissal(s))`);\n return { finalized: true, iterations: i + 1 };\n }\n }\n }\n\n log(`[claude-pair] finalize: reached ${maxIterations} iterations without ~/.claude.json being updated`);\n return { finalized: false, iterations: maxIterations };\n}\n\n/**\n * Returns true if the session is gone afterwards (either kill succeeded\n * or it was already missing). Returns false only if `kill-session`\n * failed for a non-missing-session reason — caller should treat this\n * as \"still tracked, will retry next poll\".\n */\nexport async function killPairSession(session: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['kill-session', '-t', session]);\n return true;\n } catch (err) {\n // \"can't find session\" is success-equivalent — the desired end state\n // is \"this session does not exist\" and that's already true.\n if (classifyTmuxError(err).kind === 'no-session') return true;\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Result shapes\n// ---------------------------------------------------------------------------\n\nexport type ClaudePairStartResult =\n | { kind: 'url'; url: string }\n | { kind: 'timeout' }\n | { kind: 'error'; error: SessionError };\n\nexport type ClaudePairSubmitResult =\n | { kind: 'success'; rawMatch: string }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'timeout' }\n | { kind: 'error'; error: SessionError };\n\nexport type ClaudePairStatusResult =\n | { kind: 'idle' }\n | { kind: 'awaiting-code'; url: string }\n | { kind: 'success' }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'session-missing' }\n | { kind: 'error'; error: SessionError };\n\n// ---------------------------------------------------------------------------\n// start — send `/login`, wait for the OAuth URL prompt\n// ---------------------------------------------------------------------------\n\nexport interface ClaudePairStartOpts {\n /** Pair-scoped tmux session name (see pairTmuxSession). */\n session: string;\n /** Total time to wait for Claude Code to print the URL prompt. Default 60s. */\n timeoutMs?: number;\n /** How often to re-capture and check the pane. Default 500ms. */\n pollIntervalMs?: number;\n}\n\nexport async function startClaudePair(opts: ClaudePairStartOpts): Promise<ClaudePairStartResult> {\n const { session } = opts;\n // Pair sessions cold-start `claude` from scratch — first-run on a fresh\n // host can take 10-30s before the prompt is interactive enough to\n // accept `/login`. Stay generous.\n const timeoutMs = opts.timeoutMs ?? 60_000;\n const pollIntervalMs = opts.pollIntervalMs ?? 500;\n\n // Quick precheck — fail fast if the tmux session doesn't exist\n // rather than blasting `/login` into an unrelated pane.\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Drive claude through its first-run onboarding to reach a state\n // where the OAuth URL is on screen. Possible paths:\n //\n // • Fresh host: theme picker → \"Trust folder?\" → login method picker\n // (option 1 = Claude subscription, already highlighted) → OAuth URL.\n // No `/login` needed — option 1 IS the login.\n // • Already onboarded: regular prompt → we send `/login` ourselves →\n // login method picker → OAuth URL.\n //\n // Auto-advance every onboarding screen we recognize. Keep going until\n // we either see the URL prompt or hit the deadline.\n const onboardingDeadline = Date.now() + Math.min(45_000, timeoutMs);\n let lastDispatchAt = 0;\n let loginCommandSent = false;\n const dispatchEnter = async (): Promise<void> => {\n if (Date.now() - lastDispatchAt < 1_500) return;\n lastDispatchAt = Date.now();\n try { await sendKeys(session, 'C-m'); } catch { /* keep polling */ }\n };\n\n while (Date.now() < onboardingDeadline) {\n await sleep(pollIntervalMs);\n let pane: string;\n try {\n // VIEWPORT ONLY (no scrollback). With scrollback included, the\n // theme picker / login picker text lingers in the buffer after\n // claude advances to the URL-paste prompt. The match would then\n // dispatch Enter on the empty paste field — submitting an empty\n // code and triggering \"OAuth error: Invalid code\". Visible-only\n // capture ensures we only react to what's actually on screen.\n pane = await capturePane(session, { scrollback: 0 });\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Already at the URL prompt — done driving onboarding, fall through\n // to the URL-extraction loop below.\n if (isUrlPromptReady(pane)) {\n const url = extractOAuthUrl(pane);\n if (url) return { kind: 'url', url };\n }\n\n // Stuck on \"OAuth error: Invalid code. Press Enter to retry\" — usually\n // because claude has cached partial state from a previous failed login\n // attempt on this host. Pressing Enter would just resubmit the bad\n // code and loop forever, so bail with an actionable error.\n //\n // Capture WITH scrollback for this check: claude's TUI sometimes\n // re-renders the splash/banner over the OAuth error so the viewport\n // alone misses it (we'd then time out instead of giving the operator\n // an actionable error). OAuth-retry is a sticky state — claude waits\n // for input — so checking recent scrollback is safe; a false positive\n // from stale scrollback would only fire if a previous attempt actually\n // failed, in which case bailing is the right call.\n let scrollPane = pane;\n try {\n scrollPane = await capturePane(session, { scrollback: -50 });\n } catch { /* fall back to viewport-only check */ }\n const hasOAuthInvalidCode = /OAuth error[\\s\\S]*Invalid code/i.test(scrollPane);\n const hasOAuthRetryPrompt = /OAuth error/i.test(scrollPane) && /Press Enter to retry/i.test(scrollPane);\n if (hasOAuthInvalidCode || hasOAuthRetryPrompt) {\n return {\n kind: 'error',\n error: {\n kind: 'oauth-retry-stuck',\n message:\n 'claude is stuck on a previous failed-login retry prompt. SSH to the host and clear ~/.claude/.credentials.json (and any *.json next to it), then retry pair-via-browser.',\n },\n };\n }\n\n // Login method picker — option 1 (Claude subscription) is already\n // highlighted, Enter selects it.\n if (/Select login method:/i.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // First-run theme picker. Accept the highlighted default; operator\n // can change it later via /theme.\n if (/\\bDark mode\\b/.test(pane) && /\\bLight mode\\b/.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // \"Trust this folder?\" — Enter accepts the default (Yes).\n if (/Do you trust the files in this folder\\?/i.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // Onboarding \"Press Enter to continue\" splashes.\n if (/press\\s+enter\\s+to\\s+continue/i.test(pane)) {\n await dispatchEnter();\n continue;\n }\n // Regular interactive prompt — this means claude was already\n // onboarded. Send `/login` once to surface the login picker, then\n // the next iteration handles it via the picker branch above.\n const promptVisible =\n /[│|]\\s*>\\s/.test(pane) || /Try .+ to .+/.test(pane) || /^\\s*>\\s*$/m.test(pane);\n if (promptVisible && !loginCommandSent) {\n loginCommandSent = true;\n try {\n await sendKeys(session, '/login', 'C-m');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n continue;\n }\n }\n\n // Onboarding deadline hit without seeing a URL — surface the pane so\n // operators can see what was on screen when we gave up.\n let lastPane = '';\n try { lastPane = await capturePane(session); } catch { /* best-effort */ }\n return {\n kind: 'error',\n error: {\n kind: 'unknown',\n message: `claude never reached OAuth URL prompt within ${timeoutMs}ms. Last pane: ${lastPane.slice(-500)}`,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// submit-code — paste the auth code, wait for outcome\n// ---------------------------------------------------------------------------\n\nexport interface ClaudePairSubmitOpts {\n /** Pair-scoped tmux session name (see pairTmuxSession). */\n session: string;\n code: string;\n /** Total time to wait for the success/failure marker. Default 30s. */\n timeoutMs?: number;\n pollIntervalMs?: number;\n}\n\nexport async function submitClaudePairCode(\n opts: ClaudePairSubmitOpts,\n): Promise<ClaudePairSubmitResult> {\n const { session } = opts;\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const pollIntervalMs = opts.pollIntervalMs ?? 500;\n\n // Validate code shape minimally — Claude Code's auth codes are\n // alphanumeric with dashes, ~40-80 chars. Accept anything within\n // that envelope; reject blank or whitespace-only to avoid\n // accidentally submitting an empty buffer.\n if (!opts.code || !opts.code.trim()) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'empty auth code' },\n };\n }\n if (opts.code.length > 1024) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'auth code suspiciously long' },\n };\n }\n\n // Send the code + Enter. We use the literal value as one send-keys\n // argument; tmux handles spaces fine, but newlines would terminate\n // early so reject those as well.\n if (/[\\r\\n]/.test(opts.code)) {\n return {\n kind: 'error',\n error: { kind: 'unknown', message: 'auth code contains newline' },\n };\n }\n\n // Send the code as LITERAL text (-l) so tmux doesn't try to interpret\n // any chars as key tokens. Then a 250ms breather so claude's\n // ink/React render loop finishes processing the paste before Enter\n // lands. Without that wait the Enter often gets swallowed mid-render\n // and the code stays in the input box unsubmitted.\n try {\n await execFileAsync('tmux', ['send-keys', '-t', session, '-l', opts.code.trim()]);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n await sleep(250);\n try {\n // Use Enter (semantic) plus a fallback C-m on the next iteration if\n // claude still hasn't moved.\n await sendKeys(session, 'Enter');\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n const deadline = Date.now() + timeoutMs;\n let enterRetried = false;\n let lastPane = '';\n while (Date.now() < deadline) {\n await sleep(pollIntervalMs);\n try {\n lastPane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n const outcome: AuthOutcome = detectAuthOutcome(lastPane);\n if (outcome.kind === 'success') return { kind: 'success', rawMatch: outcome.rawMatch };\n if (outcome.kind === 'failure') return { kind: 'failure', rawMatch: outcome.rawMatch };\n\n // After ~5s with no outcome, retry Enter once — covers the case\n // where the first Enter landed mid-render and claude swallowed it.\n // The \"Paste code here\" prompt is still on screen if submission\n // didn't take; if it advanced, we'd already have an outcome above.\n if (!enterRetried && Date.now() - (deadline - timeoutMs) > 5_000 && /Paste code here/i.test(lastPane)) {\n enterRetried = true;\n try { await sendKeys(session, 'C-m'); } catch { /* keep polling */ }\n }\n }\n // Include pane snippet in the error path so operators can see what\n // claude was actually showing — outcome detection is brittle and the\n // session is killed immediately after this returns, so this is our\n // only chance to capture state for debugging regex updates.\n return {\n kind: 'error',\n error: {\n kind: 'unknown',\n message: `submit timed out after ${timeoutMs}ms — outcome regex didn't match. Last pane: ${lastPane.slice(-600)}`,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// status — non-mutating peek at the pane state\n// ---------------------------------------------------------------------------\n\nexport async function getClaudePairStatus(session: string): Promise<ClaudePairStatusResult> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', session]);\n } catch (err) {\n const classified = classifyTmuxError(err);\n if (classified.kind === 'no-session') return { kind: 'session-missing' };\n return { kind: 'error', error: classified };\n }\n\n let pane: string;\n try {\n pane = await capturePane(session);\n } catch (err) {\n return { kind: 'error', error: classifyTmuxError(err) };\n }\n\n // Outcome takes priority — if the pane already shows success/failure\n // from a recent submission, the API can short-circuit without\n // restarting the flow.\n const outcome = detectAuthOutcome(pane);\n if (outcome.kind === 'success') return { kind: 'success' };\n if (outcome.kind === 'failure') return { kind: 'failure', rawMatch: outcome.rawMatch };\n\n if (isUrlPromptReady(pane)) {\n const url = extractOAuthUrl(pane);\n if (url) return { kind: 'awaiting-code', url };\n }\n return { kind: 'idle' };\n}\n","/**\n * ENG-4580: pane-scrape parser for Claude Code's `/login` OAuth flow.\n *\n * The manager drives the flow by sending `/login` into the agent's\n * persistent tmux session, capturing the pane after a short poll, and\n * extracting the OAuth URL Claude Code prints. After the operator\n * pastes the auth code via the UI, the manager sends it back into the\n * pane and polls for a success / failure marker.\n *\n * Everything in this module is pure — no tmux calls, no fs I/O. The\n * runtime side (apps/cli/src/lib/manager-worker.ts) shells out and\n * feeds the captured pane string through these functions.\n *\n * Why a dedicated module: pane scraping is fragile across Claude Code\n * versions, terminal widths, and locale changes. Centralising the\n * regexes + the ANSI stripper makes them easy to fixture-test and\n * iterate on without touching the runtime path.\n */\n\n// ---------------------------------------------------------------------------\n// ANSI escape sequence stripper\n// ---------------------------------------------------------------------------\n\n/**\n * Strip the ANSI escape sequences a terminal emits for colour, cursor\n * movement, screen clears, and bracketed paste mode. The pattern below\n * covers:\n *\n * - CSI sequences: `ESC [ ... <final byte>` where the final byte is\n * in the 0x40-0x7E range (covers SGR colour, cursor-position,\n * erase-in-line/display, etc.)\n * - OSC sequences: `ESC ] ... BEL` or `ESC ] ... ESC \\` (used for\n * window titles and hyperlinks)\n * - Single-character `ESC <char>` two-byte escapes (e.g. `ESC =`,\n * `ESC >`, the `ESC c` reset)\n *\n * We keep newlines and printable text intact so pane content remains\n * matchable after stripping.\n *\n * The regex uses Unicode-friendly character classes; we explicitly\n * avoid `\\x1b` named escapes in source to keep the file ASCII-safe.\n */\nconst ANSI_ESC = String.fromCharCode(0x1b);\nconst ANSI_BEL = String.fromCharCode(0x07);\n\nconst CSI_RE = new RegExp(`${ANSI_ESC}\\\\[[0-?]*[ -/]*[@-~]`, 'g');\nconst OSC_RE = new RegExp(\n `${ANSI_ESC}\\\\][^${ANSI_BEL}${ANSI_ESC}]*(?:${ANSI_BEL}|${ANSI_ESC}\\\\\\\\)`,\n 'g',\n);\nconst TWO_BYTE_RE = new RegExp(`${ANSI_ESC}[=>cM78]`, 'g');\n\nexport function stripAnsi(text: string): string {\n return text.replace(CSI_RE, '').replace(OSC_RE, '').replace(TWO_BYTE_RE, '');\n}\n\n// ---------------------------------------------------------------------------\n// OAuth URL extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Anchored to Anthropic-owned domains that Claude Code's `/login`\n * actually prints. Adding more hosts is fine — keep them allowlisted\n * rather than matching arbitrary `https://` to avoid pulling random\n * URLs from the user's previous shell output.\n *\n * Note: Claude Code currently emits URLs at `claude.com/cai/oauth/...`\n * (the consumer-facing domain), but `claude.ai` and the console hosts\n * have appeared historically and are kept here for tolerance.\n */\nconst OAUTH_URL_RE =\n /https:\\/\\/(?:claude\\.com|claude\\.ai|platform\\.claude\\.com|console\\.anthropic\\.com|auth\\.anthropic\\.com)\\/[^\\s)\\]]*/;\n\n/**\n * Strip ANSI + reassemble URLs that were soft-wrapped across terminal\n * lines. tmux capture-pane emits hard newlines for wrapped lines, and\n * Claude Code's OAuth URL routinely runs >300 chars — much wider than\n * the manager's default tmux window. Iteratively strip newlines that\n * fall inside what looks like a URL until convergence (a single URL\n * can wrap 5+ times).\n *\n * Shared between extractOAuthUrl and isUrlPromptReady so the readiness\n * check sees the same string the extractor would.\n */\nfunction dewrapPane(rawPane: string): string {\n const stripped = stripAnsi(rawPane);\n let dewrapped = stripped;\n let prev = '';\n while (prev !== dewrapped) {\n prev = dewrapped;\n dewrapped = dewrapped.replace(/(https?:\\/\\/\\S+?)\\n(?=\\S)/, (_m, head: string) => head);\n }\n return dewrapped;\n}\n\nexport function extractOAuthUrl(rawPane: string): string | null {\n const match = OAUTH_URL_RE.exec(dewrapPane(rawPane));\n if (!match) return null;\n // Trim trailing punctuation that often clings to URLs in TUIs.\n return match[0].replace(/[.,;:!?]+$/, '');\n}\n\n// ---------------------------------------------------------------------------\n// Prompt readiness — \"we've printed the URL, now waiting for a code\"\n// ---------------------------------------------------------------------------\n\nconst URL_PROMPT_RE =\n /(?:Paste code here|Paste your code|Enter (?:the )?code|Authorization code)/i;\n\nexport function isUrlPromptReady(rawPane: string): boolean {\n const dewrapped = dewrapPane(rawPane);\n // Both anchors must be present: the URL itself AND the paste-code\n // prompt. The prompt alone could appear during a stale screen redraw;\n // the URL alone could be a stray match in command history. Use the\n // dewrapped pane so a wrapped URL still matches OAUTH_URL_RE.\n return OAUTH_URL_RE.test(dewrapped) && URL_PROMPT_RE.test(dewrapped);\n}\n\n// ---------------------------------------------------------------------------\n// Outcome detection after submitting the code\n// ---------------------------------------------------------------------------\n\n// Claude Code's success/failure copy has drifted across versions\n// (\"Logged in\" → \"Login successful\" → \"Signed in as ...\" → etc.).\n// Keep the alternations broad-but-specific — phrases that only appear\n// after a real auth roundtrip, never in welcome / tutorial text.\n// \"Welcome back\" was tried and rejected: it shows up in onboarding\n// help blurbs and produced false positives.\nconst SUCCESS_RE =\n /(?:Logged in|Login successful|Successfully (?:logged in|authenticated|signed in)|Authentication successful|Sign-?in (?:complete|successful)|You(?:'|’)?re signed in|Signed in as)/i;\nconst FAILURE_RE =\n /(?:Invalid (?:code|authorization code)|OAuth error|Authentication failed|Error (?:logging in|during authentication)|Login failed|Sign-?in failed|Failed to (?:authenticate|sign in|log in))/i;\n\nexport type AuthOutcome =\n | { kind: 'success'; rawMatch: string }\n | { kind: 'failure'; rawMatch: string }\n | { kind: 'pending' };\n\nexport function detectAuthOutcome(rawPane: string): AuthOutcome {\n const stripped = stripAnsi(rawPane);\n // Failure first — Claude Code sometimes prints a stale \"logged in\" from\n // a previous successful session above the new failure banner. The\n // most-recent line wins, so we scan from the end of the pane.\n const failureMatch = lastMatch(stripped, FAILURE_RE);\n const successMatch = lastMatch(stripped, SUCCESS_RE);\n\n if (failureMatch && successMatch) {\n // Whichever is later on the pane is the live state.\n if (failureMatch.index > successMatch.index) {\n return { kind: 'failure', rawMatch: failureMatch.match };\n }\n return { kind: 'success', rawMatch: successMatch.match };\n }\n if (failureMatch) return { kind: 'failure', rawMatch: failureMatch.match };\n if (successMatch) return { kind: 'success', rawMatch: successMatch.match };\n return { kind: 'pending' };\n}\n\nfunction lastMatch(haystack: string, re: RegExp): { match: string; index: number } | null {\n // Construct a sticky/global variant if needed. Most of our REs are\n // anchored to small phrases; iterating with a `g`-flagged RegExp is\n // cheap and correct.\n const globalRe = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);\n let last: RegExpExecArray | null = null;\n let m: RegExpExecArray | null;\n while ((m = globalRe.exec(haystack)) !== null) {\n last = m;\n // Prevent zero-length matches from looping.\n if (m.index === globalRe.lastIndex) globalRe.lastIndex++;\n }\n return last ? { match: last[0], index: last.index } : null;\n}\n\n// ---------------------------------------------------------------------------\n// \"Session not running / tmux missing\" — surface as a structured signal\n// ---------------------------------------------------------------------------\n\n/**\n * The runtime path will throw when `tmux capture-pane` fails. This\n * helper classifies the failure for the API layer so the UI can show\n * \"start a session first\" rather than a generic 500.\n */\nexport type SessionError =\n | { kind: 'no-session' }\n | { kind: 'tmux-missing' }\n | { kind: 'pane-empty' }\n /** Claude is sitting on the \"OAuth error: Invalid code. Press Enter to\n * retry.\" prompt from a previous failed login — pressing Enter would\n * resubmit the bad code, so the runtime bails with operator-actionable\n * guidance instead. Distinct from `unknown` so telemetry / UI can\n * surface a specific message rather than a generic 500. */\n | { kind: 'oauth-retry-stuck'; message: string }\n | { kind: 'unknown'; message: string };\n\nexport function classifyTmuxError(err: unknown): SessionError {\n const msg = err instanceof Error ? err.message : String(err);\n if (/can't find session|no server running/i.test(msg)) return { kind: 'no-session' };\n if (/command not found.*tmux|ENOENT.*tmux/i.test(msg)) return { kind: 'tmux-missing' };\n return { kind: 'unknown', message: msg };\n}\n"],"mappings":";AAmBA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAC1B,SAAS,YAAY,gBAAgB;AACrC,SAAS,YAAY;AACrB,SAAS,eAAe;;;ACmBxB,IAAM,WAAW,OAAO,aAAa,EAAI;AACzC,IAAM,WAAW,OAAO,aAAa,CAAI;AAEzC,IAAM,SAAS,IAAI,OAAO,GAAG,QAAQ,wBAAwB,GAAG;AAChE,IAAM,SAAS,IAAI;AAAA,EACjB,GAAG,QAAQ,QAAQ,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,IAAI,QAAQ;AAAA,EAClE;AACF;AACA,IAAM,cAAc,IAAI,OAAO,GAAG,QAAQ,YAAY,GAAG;AAElD,SAAS,UAAU,MAAsB;AAC9C,SAAO,KAAK,QAAQ,QAAQ,EAAE,EAAE,QAAQ,QAAQ,EAAE,EAAE,QAAQ,aAAa,EAAE;AAC7E;AAgBA,IAAM,eACJ;AAaF,SAAS,WAAW,SAAyB;AAC3C,QAAM,WAAW,UAAU,OAAO;AAClC,MAAI,YAAY;AAChB,MAAI,OAAO;AACX,SAAO,SAAS,WAAW;AACzB,WAAO;AACP,gBAAY,UAAU,QAAQ,6BAA6B,CAAC,IAAI,SAAiB,IAAI;AAAA,EACvF;AACA,SAAO;AACT;AAEO,SAAS,gBAAgB,SAAgC;AAC9D,QAAM,QAAQ,aAAa,KAAK,WAAW,OAAO,CAAC;AACnD,MAAI,CAAC,MAAO,QAAO;AAEnB,SAAO,MAAM,CAAC,EAAE,QAAQ,cAAc,EAAE;AAC1C;AAMA,IAAM,gBACJ;AAEK,SAAS,iBAAiB,SAA0B;AACzD,QAAM,YAAY,WAAW,OAAO;AAKpC,SAAO,aAAa,KAAK,SAAS,KAAK,cAAc,KAAK,SAAS;AACrE;AAYA,IAAM,aACJ;AACF,IAAM,aACJ;AAOK,SAAS,kBAAkB,SAA8B;AAC9D,QAAM,WAAW,UAAU,OAAO;AAIlC,QAAM,eAAe,UAAU,UAAU,UAAU;AACnD,QAAM,eAAe,UAAU,UAAU,UAAU;AAEnD,MAAI,gBAAgB,cAAc;AAEhC,QAAI,aAAa,QAAQ,aAAa,OAAO;AAC3C,aAAO,EAAE,MAAM,WAAW,UAAU,aAAa,MAAM;AAAA,IACzD;AACA,WAAO,EAAE,MAAM,WAAW,UAAU,aAAa,MAAM;AAAA,EACzD;AACA,MAAI,aAAc,QAAO,EAAE,MAAM,WAAW,UAAU,aAAa,MAAM;AACzE,MAAI,aAAc,QAAO,EAAE,MAAM,WAAW,UAAU,aAAa,MAAM;AACzE,SAAO,EAAE,MAAM,UAAU;AAC3B;AAEA,SAAS,UAAU,UAAkB,IAAqD;AAIxF,QAAM,WAAW,IAAI,OAAO,GAAG,QAAQ,GAAG,MAAM,SAAS,GAAG,IAAI,GAAG,QAAQ,GAAG,GAAG,KAAK,GAAG;AACzF,MAAI,OAA+B;AACnC,MAAI;AACJ,UAAQ,IAAI,SAAS,KAAK,QAAQ,OAAO,MAAM;AAC7C,WAAO;AAEP,QAAI,EAAE,UAAU,SAAS,UAAW,UAAS;AAAA,EAC/C;AACA,SAAO,OAAO,EAAE,OAAO,KAAK,CAAC,GAAG,OAAO,KAAK,MAAM,IAAI;AACxD;AAuBO,SAAS,kBAAkB,KAA4B;AAC5D,QAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,MAAI,wCAAwC,KAAK,GAAG,EAAG,QAAO,EAAE,MAAM,aAAa;AACnF,MAAI,wCAAwC,KAAK,GAAG,EAAG,QAAO,EAAE,MAAM,eAAe;AACrF,SAAO,EAAE,MAAM,WAAW,SAAS,IAAI;AACzC;;;ADrKA,IAAM,gBAAgB,UAAU,QAAQ;AAMjC,SAAS,gBAAgB,QAAwB;AACtD,SAAO,YAAY,OAAO,MAAM,GAAG,EAAE,CAAC;AACxC;AAWA,eAAe,YAAY,SAAiB,OAAwB,CAAC,GAAoB;AACvF,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,EAAE,OAAO,IAAI,MAAM,cAAc,QAAQ;AAAA,IAC7C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,UAAU;AAAA,EACnB,CAAC;AACD,SAAO;AACT;AAEA,eAAe,SAAS,YAAoB,MAA+B;AACzE,QAAM,cAAc,QAAQ,CAAC,aAAa,MAAM,SAAS,GAAG,IAAI,CAAC;AACnE;AAEA,eAAe,MAAM,IAA2B;AAC9C,SAAO,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC7C;AAiBA,eAAsB,iBAAiB,SAA6E;AAClH,MAAI;AACF,UAAM,cAAc,QAAQ,CAAC,eAAe,MAAM,OAAO,CAAC;AAC1D,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB,QAAQ;AAAA,EAER;AAEA,QAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,kCAAyB;AACtE,QAAM,YAAY,oBAAoB;AAEtC,MAAI;AAMF,UAAM,cAAc,QAAQ;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,OAAO,kBAAkB,GAAG,EAAE;AAAA,EACpD;AAQA,QAAM,MAAM,GAAG;AACf,MAAI;AACF,UAAM,cAAc,QAAQ,CAAC,eAAe,MAAM,OAAO,CAAC;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,qDAAqD,SAAS,YAAY,SAAS;AAAA,MAC9F;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,IAAI,KAAK;AACpB;AAkCA,eAAsB,6BACpB,SACA,KACA,OAAiF,CAAC,GAC7B;AACrD,QAAM,gBAAgB,KAAK,iBAAiB;AAC5C,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,iBAAiB,KAAK,kBAAkB,KAAK,QAAQ,GAAG,cAAc;AAM5E,QAAM,eAAe,WAAW,cAAc,IAC1C,SAAS,cAAc,EAAE,UACzB;AAEJ,WAAS,IAAI,GAAG,IAAI,eAAe,KAAK;AACtC,UAAM,MAAM,UAAU;AACtB,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,YAAY,OAAO;AAAA,IAClC,SAAS,KAAK;AAKZ,YAAM,aAAa,kBAAkB,GAAG;AACxC,UAAI,gDAAgD,WAAW,IAAI,6BAA6B;AAChG,aAAO,EAAE,WAAW,OAAO,YAAY,EAAE;AAAA,IAC3C;AAMA,QACE,KAAK,SAAS,kBAAkB,KAChC,KAAK,SAAS,cAAc,KAC5B,2BAA2B,KAAK,IAAI,KACpC,KAAK,SAAS,gBAAgB,GAC9B;AACA,UAAI;AACF,cAAM,SAAS,SAAS,KAAK;AAAA,MAC/B,QAAQ;AAEN,YAAI,oEAAoE;AACxE,eAAO,EAAE,WAAW,OAAO,YAAY,EAAE;AAAA,MAC3C;AACA;AAAA,IACF;AAQA,QAAI,WAAW,cAAc,GAAG;AAC9B,YAAM,QAAQ,SAAS,cAAc,EAAE;AACvC,UAAI,QAAQ,cAAc;AACxB,YAAI,yDAAyD,IAAI,CAAC,uBAAuB;AACzF,eAAO,EAAE,WAAW,MAAM,YAAY,IAAI,EAAE;AAAA,MAC9C;AAAA,IACF;AAAA,EACF;AAEA,MAAI,mCAAmC,aAAa,kDAAkD;AACtG,SAAO,EAAE,WAAW,OAAO,YAAY,cAAc;AACvD;AAQA,eAAsB,gBAAgB,SAAmC;AACvE,MAAI;AACF,UAAM,cAAc,QAAQ,CAAC,gBAAgB,MAAM,OAAO,CAAC;AAC3D,WAAO;AAAA,EACT,SAAS,KAAK;AAGZ,QAAI,kBAAkB,GAAG,EAAE,SAAS,aAAc,QAAO;AACzD,WAAO;AAAA,EACT;AACF;AAsCA,eAAsB,gBAAgB,MAA2D;AAC/F,QAAM,EAAE,QAAQ,IAAI;AAIpB,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,iBAAiB,KAAK,kBAAkB;AAI9C,MAAI;AACF,UAAM,cAAc,QAAQ,CAAC,eAAe,MAAM,OAAO,CAAC;AAAA,EAC5D,SAAS,KAAK;AACZ,WAAO,EAAE,MAAM,SAAS,OAAO,kBAAkB,GAAG,EAAE;AAAA,EACxD;AAaA,QAAM,qBAAqB,KAAK,IAAI,IAAI,KAAK,IAAI,MAAQ,SAAS;AAClE,MAAI,iBAAiB;AACrB,MAAI,mBAAmB;AACvB,QAAM,gBAAgB,YAA2B;AAC/C,QAAI,KAAK,IAAI,IAAI,iBAAiB,KAAO;AACzC,qBAAiB,KAAK,IAAI;AAC1B,QAAI;AAAE,YAAM,SAAS,SAAS,KAAK;AAAA,IAAG,QAAQ;AAAA,IAAqB;AAAA,EACrE;AAEA,SAAO,KAAK,IAAI,IAAI,oBAAoB;AACtC,UAAM,MAAM,cAAc;AAC1B,QAAI;AACJ,QAAI;AAOF,aAAO,MAAM,YAAY,SAAS,EAAE,YAAY,EAAE,CAAC;AAAA,IACrD,SAAS,KAAK;AACZ,aAAO,EAAE,MAAM,SAAS,OAAO,kBAAkB,GAAG,EAAE;AAAA,IACxD;AAIA,QAAI,iBAAiB,IAAI,GAAG;AAC1B,YAAM,MAAM,gBAAgB,IAAI;AAChC,UAAI,IAAK,QAAO,EAAE,MAAM,OAAO,IAAI;AAAA,IACrC;AAcA,QAAI,aAAa;AACjB,QAAI;AACF,mBAAa,MAAM,YAAY,SAAS,EAAE,YAAY,IAAI,CAAC;AAAA,IAC7D,QAAQ;AAAA,IAAyC;AACjD,UAAM,sBAAsB,kCAAkC,KAAK,UAAU;AAC7E,UAAM,sBAAsB,eAAe,KAAK,UAAU,KAAK,wBAAwB,KAAK,UAAU;AACtG,QAAI,uBAAuB,qBAAqB;AAC9C,aAAO;AAAA,QACL,MAAM;AAAA,QACN,OAAO;AAAA,UACL,MAAM;AAAA,UACN,SACE;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAIA,QAAI,wBAAwB,KAAK,IAAI,GAAG;AACtC,YAAM,cAAc;AACpB;AAAA,IACF;AAGA,QAAI,gBAAgB,KAAK,IAAI,KAAK,iBAAiB,KAAK,IAAI,GAAG;AAC7D,YAAM,cAAc;AACpB;AAAA,IACF;AAEA,QAAI,2CAA2C,KAAK,IAAI,GAAG;AACzD,YAAM,cAAc;AACpB;AAAA,IACF;AAEA,QAAI,iCAAiC,KAAK,IAAI,GAAG;AAC/C,YAAM,cAAc;AACpB;AAAA,IACF;AAIA,UAAM,gBACJ,aAAa,KAAK,IAAI,KAAK,eAAe,KAAK,IAAI,KAAK,aAAa,KAAK,IAAI;AAChF,QAAI,iBAAiB,CAAC,kBAAkB;AACtC,yBAAmB;AACnB,UAAI;AACF,cAAM,SAAS,SAAS,UAAU,KAAK;AAAA,MACzC,SAAS,KAAK;AACZ,eAAO,EAAE,MAAM,SAAS,OAAO,kBAAkB,GAAG,EAAE;AAAA,MACxD;AACA;AAAA,IACF;AAAA,EACF;AAIA,MAAI,WAAW;AACf,MAAI;AAAE,eAAW,MAAM,YAAY,OAAO;AAAA,EAAG,QAAQ;AAAA,EAAoB;AACzE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,gDAAgD,SAAS,kBAAkB,SAAS,MAAM,IAAI,CAAC;AAAA,IAC1G;AAAA,EACF;AACF;AAeA,eAAsB,qBACpB,MACiC;AACjC,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,iBAAiB,KAAK,kBAAkB;AAM9C,MAAI,CAAC,KAAK,QAAQ,CAAC,KAAK,KAAK,KAAK,GAAG;AACnC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO,EAAE,MAAM,WAAW,SAAS,kBAAkB;AAAA,IACvD;AAAA,EACF;AACA,MAAI,KAAK,KAAK,SAAS,MAAM;AAC3B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO,EAAE,MAAM,WAAW,SAAS,8BAA8B;AAAA,IACnE;AAAA,EACF;AAKA,MAAI,SAAS,KAAK,KAAK,IAAI,GAAG;AAC5B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO,EAAE,MAAM,WAAW,SAAS,6BAA6B;AAAA,IAClE;AAAA,EACF;AAOA,MAAI;AACF,UAAM,cAAc,QAAQ,CAAC,aAAa,MAAM,SAAS,MAAM,KAAK,KAAK,KAAK,CAAC,CAAC;AAAA,EAClF,SAAS,KAAK;AACZ,WAAO,EAAE,MAAM,SAAS,OAAO,kBAAkB,GAAG,EAAE;AAAA,EACxD;AACA,QAAM,MAAM,GAAG;AACf,MAAI;AAGF,UAAM,SAAS,SAAS,OAAO;AAAA,EACjC,SAAS,KAAK;AACZ,WAAO,EAAE,MAAM,SAAS,OAAO,kBAAkB,GAAG,EAAE;AAAA,EACxD;AAEA,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,MAAI,eAAe;AACnB,MAAI,WAAW;AACf,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,UAAM,MAAM,cAAc;AAC1B,QAAI;AACF,iBAAW,MAAM,YAAY,OAAO;AAAA,IACtC,SAAS,KAAK;AACZ,aAAO,EAAE,MAAM,SAAS,OAAO,kBAAkB,GAAG,EAAE;AAAA,IACxD;AACA,UAAM,UAAuB,kBAAkB,QAAQ;AACvD,QAAI,QAAQ,SAAS,UAAW,QAAO,EAAE,MAAM,WAAW,UAAU,QAAQ,SAAS;AACrF,QAAI,QAAQ,SAAS,UAAW,QAAO,EAAE,MAAM,WAAW,UAAU,QAAQ,SAAS;AAMrF,QAAI,CAAC,gBAAgB,KAAK,IAAI,KAAK,WAAW,aAAa,OAAS,mBAAmB,KAAK,QAAQ,GAAG;AACrG,qBAAe;AACf,UAAI;AAAE,cAAM,SAAS,SAAS,KAAK;AAAA,MAAG,QAAQ;AAAA,MAAqB;AAAA,IACrE;AAAA,EACF;AAKA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,0BAA0B,SAAS,oDAA+C,SAAS,MAAM,IAAI,CAAC;AAAA,IACjH;AAAA,EACF;AACF;AAMA,eAAsB,oBAAoB,SAAkD;AAC1F,MAAI;AACF,UAAM,cAAc,QAAQ,CAAC,eAAe,MAAM,OAAO,CAAC;AAAA,EAC5D,SAAS,KAAK;AACZ,UAAM,aAAa,kBAAkB,GAAG;AACxC,QAAI,WAAW,SAAS,aAAc,QAAO,EAAE,MAAM,kBAAkB;AACvE,WAAO,EAAE,MAAM,SAAS,OAAO,WAAW;AAAA,EAC5C;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,YAAY,OAAO;AAAA,EAClC,SAAS,KAAK;AACZ,WAAO,EAAE,MAAM,SAAS,OAAO,kBAAkB,GAAG,EAAE;AAAA,EACxD;AAKA,QAAM,UAAU,kBAAkB,IAAI;AACtC,MAAI,QAAQ,SAAS,UAAW,QAAO,EAAE,MAAM,UAAU;AACzD,MAAI,QAAQ,SAAS,UAAW,QAAO,EAAE,MAAM,WAAW,UAAU,QAAQ,SAAS;AAErF,MAAI,iBAAiB,IAAI,GAAG;AAC1B,UAAM,MAAM,gBAAgB,IAAI;AAChC,QAAI,IAAK,QAAO,EAAE,MAAM,iBAAiB,IAAI;AAAA,EAC/C;AACA,SAAO,EAAE,MAAM,OAAO;AACxB;","names":[]}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
resolveChannels,
|
|
23
23
|
resolveDmTarget,
|
|
24
24
|
wrapScheduledTaskPrompt
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-MEJGM5RV.js";
|
|
26
26
|
import {
|
|
27
27
|
findTaskByTemplate,
|
|
28
28
|
getProjectDir,
|
|
@@ -33,16 +33,21 @@ import {
|
|
|
33
33
|
} from "../chunk-M6FSTVGG.js";
|
|
34
34
|
import {
|
|
35
35
|
buildAllowedTools,
|
|
36
|
+
getLastFailureContext,
|
|
36
37
|
getProjectDir as getProjectDir2,
|
|
37
38
|
injectMessage,
|
|
39
|
+
isAgentIdle,
|
|
38
40
|
isSessionHealthy,
|
|
41
|
+
isStaleForToday,
|
|
42
|
+
peekCurrentSession,
|
|
43
|
+
prepareForRespawn,
|
|
39
44
|
resetRestartCount,
|
|
40
45
|
resolveClaudeBinary,
|
|
41
46
|
sanitizeMcpJson,
|
|
42
47
|
startPersistentSession,
|
|
43
48
|
stopAllSessionsAndWait,
|
|
44
49
|
stopPersistentSession
|
|
45
|
-
} from "../chunk-
|
|
50
|
+
} from "../chunk-EG5D3KUV.js";
|
|
46
51
|
|
|
47
52
|
// src/lib/manager-worker.ts
|
|
48
53
|
import { createHash } from "crypto";
|
|
@@ -794,6 +799,44 @@ async function sweepChannelProcesses(opts) {
|
|
|
794
799
|
dryRun
|
|
795
800
|
};
|
|
796
801
|
}
|
|
802
|
+
function pickTeardownTargets(processes, codeName) {
|
|
803
|
+
return processes.filter((p) => p.codeName === codeName);
|
|
804
|
+
}
|
|
805
|
+
function defaultKillSignal(pid, signal) {
|
|
806
|
+
try {
|
|
807
|
+
process.kill(pid, signal);
|
|
808
|
+
} catch {
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function defaultPs() {
|
|
812
|
+
return execFileSync("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
|
|
813
|
+
encoding: "utf-8",
|
|
814
|
+
timeout: 5e3,
|
|
815
|
+
maxBuffer: 10 * 1024 * 1024
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function killAgentChannelProcesses(codeName, opts) {
|
|
819
|
+
const { log: log2, dryRun = false } = opts;
|
|
820
|
+
const kill = opts.killFn ?? defaultKillSignal;
|
|
821
|
+
const ps = opts.psFn ?? defaultPs;
|
|
822
|
+
let psOutput = "";
|
|
823
|
+
try {
|
|
824
|
+
psOutput = ps();
|
|
825
|
+
} catch (err) {
|
|
826
|
+
log2(`[channel-teardown] ps failed for '${codeName}': ${err.message}`);
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
const targets = pickTeardownTargets(parsePsOutput(psOutput), codeName);
|
|
830
|
+
if (targets.length === 0) return [];
|
|
831
|
+
const pids = targets.map((t) => t.pid);
|
|
832
|
+
log2(
|
|
833
|
+
`[channel-teardown]${dryRun ? "[dry-run]" : ""} de-provision '${codeName}': killing ${targets.length} channel MCP(s) \u2014 ` + targets.map((t) => `${t.channelType}#${t.pid}`).join(", ")
|
|
834
|
+
);
|
|
835
|
+
if (!dryRun) {
|
|
836
|
+
for (const pid of pids) kill(pid, "SIGTERM");
|
|
837
|
+
}
|
|
838
|
+
return pids;
|
|
839
|
+
}
|
|
797
840
|
|
|
798
841
|
// src/lib/delivery-hint.ts
|
|
799
842
|
var DEFAULT_PROBABILITY = 0.1;
|
|
@@ -1361,6 +1404,12 @@ var lastChannelSweepAt = 0;
|
|
|
1361
1404
|
var config = null;
|
|
1362
1405
|
var running = false;
|
|
1363
1406
|
var pollTimer = null;
|
|
1407
|
+
var PANE_TAIL_PREVIEW_LINES = 5;
|
|
1408
|
+
function truncateForLog(s) {
|
|
1409
|
+
const lines = s.split("\n").filter((l) => l.length > 0);
|
|
1410
|
+
return lines.slice(-PANE_TAIL_PREVIEW_LINES).map((l) => ` | ${l}`).join("\n");
|
|
1411
|
+
}
|
|
1412
|
+
var KNOWN_SAFE_TAIL_SIGNATURES = /* @__PURE__ */ new Set(["session_id_in_use"]);
|
|
1364
1413
|
var knownVersions = /* @__PURE__ */ new Map();
|
|
1365
1414
|
var knownStatuses = /* @__PURE__ */ new Map();
|
|
1366
1415
|
var knownChannels = /* @__PURE__ */ new Map();
|
|
@@ -1435,7 +1484,7 @@ function clearAgentCaches(agentId, codeName) {
|
|
|
1435
1484
|
var cachedFrameworkVersion = null;
|
|
1436
1485
|
var lastVersionCheckAt = 0;
|
|
1437
1486
|
var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1438
|
-
var agtCliVersion = true ? "0.16.
|
|
1487
|
+
var agtCliVersion = true ? "0.16.2" : "dev";
|
|
1439
1488
|
function resolveBrewPath(execFileSync2) {
|
|
1440
1489
|
try {
|
|
1441
1490
|
const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
@@ -2312,7 +2361,7 @@ async function pollCycle() {
|
|
|
2312
2361
|
}
|
|
2313
2362
|
try {
|
|
2314
2363
|
const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
|
|
2315
|
-
const { collectDiagnostics } = await import("../persistent-session-
|
|
2364
|
+
const { collectDiagnostics } = await import("../persistent-session-YEUFJMWF.js");
|
|
2316
2365
|
const diagCodeNames = [...persistentSessionAgents];
|
|
2317
2366
|
const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
|
|
2318
2367
|
let tailscaleHostname;
|
|
@@ -2412,6 +2461,13 @@ async function pollCycle() {
|
|
|
2412
2461
|
log(`Agent '${prev.codeName}' removed from host (deleted or unassigned)`);
|
|
2413
2462
|
const adapter = resolveAgentFramework(prev.codeName);
|
|
2414
2463
|
await stopGatewayIfRunning(prev.codeName, adapter);
|
|
2464
|
+
stopPersistentSession(prev.codeName, log);
|
|
2465
|
+
try {
|
|
2466
|
+
const { execSync: es } = await import("child_process");
|
|
2467
|
+
es(`tmux kill-session -t agt-${prev.codeName} 2>/dev/null`, { stdio: "ignore" });
|
|
2468
|
+
} catch {
|
|
2469
|
+
}
|
|
2470
|
+
killAgentChannelProcesses(prev.codeName, { log });
|
|
2415
2471
|
freePort(prev.codeName);
|
|
2416
2472
|
const agentDir = join3(adapter.getAgentDir(prev.codeName), "provision");
|
|
2417
2473
|
await cleanupAgentFiles(prev.codeName, agentDir);
|
|
@@ -2539,6 +2595,7 @@ async function processAgent(agent, agentStates) {
|
|
|
2539
2595
|
es(`tmux kill-session -t agt-${agent.code_name} 2>/dev/null`, { stdio: "ignore" });
|
|
2540
2596
|
} catch {
|
|
2541
2597
|
}
|
|
2598
|
+
killAgentChannelProcesses(agent.code_name, { log });
|
|
2542
2599
|
freePort(agent.code_name);
|
|
2543
2600
|
await cleanupAgentFiles(agent.code_name, agentDir);
|
|
2544
2601
|
clearAgentCaches(agent.agent_id, agent.code_name);
|
|
@@ -3778,6 +3835,24 @@ async function finishRun(runId, outcome, options = {}) {
|
|
|
3778
3835
|
log(`[runs] finish failed for run_id=${runId} outcome=${outcome} error_id=${errId}`);
|
|
3779
3836
|
}
|
|
3780
3837
|
}
|
|
3838
|
+
var MAX_PRIOR_RUNS = 5;
|
|
3839
|
+
async function fetchPriorScheduledRuns(agentId, taskId) {
|
|
3840
|
+
try {
|
|
3841
|
+
const data = await api.post("/host/scheduled-tasks/recent-outputs", {
|
|
3842
|
+
agent_id: agentId,
|
|
3843
|
+
task_id: taskId,
|
|
3844
|
+
since_hours: 24,
|
|
3845
|
+
limit: MAX_PRIOR_RUNS
|
|
3846
|
+
});
|
|
3847
|
+
const rows = Array.isArray(data?.runs) ? data.runs.slice(0, MAX_PRIOR_RUNS) : [];
|
|
3848
|
+
return rows.filter((r) => typeof r.output_text === "string" && r.output_text.length > 0).map((r) => ({ startedAt: r.started_at, output: r.output_text }));
|
|
3849
|
+
} catch (err) {
|
|
3850
|
+
const errText = err instanceof Error ? err.message : String(err);
|
|
3851
|
+
const errId = createHash("sha256").update(errText).digest("hex").slice(0, 12);
|
|
3852
|
+
log(`[runs] prior-runs lookup failed for task_id=${taskId} error_id=${errId}`);
|
|
3853
|
+
return [];
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3781
3856
|
async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
3782
3857
|
const projectDir = getProjectDir(codeName);
|
|
3783
3858
|
const mcpConfigPath = join3(projectDir, ".mcp.json");
|
|
@@ -3785,7 +3860,8 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
|
3785
3860
|
let kanbanItemId = null;
|
|
3786
3861
|
let taskResult;
|
|
3787
3862
|
sanitizeMcpJson(mcpConfigPath, requireHost());
|
|
3788
|
-
|
|
3863
|
+
const priorRuns = await fetchPriorScheduledRuns(agentId, task.taskId);
|
|
3864
|
+
prompt = wrapScheduledTaskPrompt(prompt, { priorRuns });
|
|
3789
3865
|
try {
|
|
3790
3866
|
const claudeMdPath = join3(projectDir, "CLAUDE.md");
|
|
3791
3867
|
const serverNames = [];
|
|
@@ -4061,9 +4137,34 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
|
|
|
4061
4137
|
stopPersistentSession(codeName, log);
|
|
4062
4138
|
persistentSessionAgents.delete(codeName);
|
|
4063
4139
|
}
|
|
4140
|
+
if (isStaleForToday(codeName) && isSessionHealthy(codeName)) {
|
|
4141
|
+
const current = peekCurrentSession(codeName);
|
|
4142
|
+
if (current) {
|
|
4143
|
+
const idle = isAgentIdle(projectDir, current.sessionId);
|
|
4144
|
+
if (idle) {
|
|
4145
|
+
log(
|
|
4146
|
+
`[persistent-session] Day rollover for '${codeName}' (yesterday=${current.date}) \u2014 agent idle, restarting to mint fresh session`
|
|
4147
|
+
);
|
|
4148
|
+
stopPersistentSession(codeName, log);
|
|
4149
|
+
persistentSessionAgents.delete(codeName);
|
|
4150
|
+
} else {
|
|
4151
|
+
log(
|
|
4152
|
+
`[persistent-session] Day rollover for '${codeName}' deferred \u2014 agent still active on session ${current.sessionId} (will retry next tick)`
|
|
4153
|
+
);
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4064
4157
|
if (!isSessionHealthy(codeName)) {
|
|
4065
4158
|
if (persistentSessionAgents.has(codeName)) {
|
|
4066
|
-
|
|
4159
|
+
const ctx = getLastFailureContext(codeName);
|
|
4160
|
+
const recovery = prepareForRespawn(codeName);
|
|
4161
|
+
const tailSummary = !ctx.tail ? "" : KNOWN_SAFE_TAIL_SIGNATURES.has(ctx.signature) ? `; last pane output (${PANE_TAIL_PREVIEW_LINES} of ~20 lines):
|
|
4162
|
+
${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash("sha256").update(ctx.tail).digest("hex").slice(0, 12)} (raw at ~/.augmented/${codeName}/pane.log)`;
|
|
4163
|
+
const sigSummary = ctx.signature !== "unknown" ? `; signature=${ctx.signature}` : "";
|
|
4164
|
+
const recoverySummary = recovery ? `; recovery=${recovery}` : "";
|
|
4165
|
+
log(
|
|
4166
|
+
`[persistent-session] Session for '${codeName}' is unhealthy (restart #${ctx.restartCount}${sigSummary}${recoverySummary}), will restart${tailSummary}`
|
|
4167
|
+
);
|
|
4067
4168
|
}
|
|
4068
4169
|
try {
|
|
4069
4170
|
provisionStopHook(codeName);
|
|
@@ -4288,6 +4389,7 @@ function ensureRealtimeAssignStarted(agentStates) {
|
|
|
4288
4389
|
hostId: exchange.hostId,
|
|
4289
4390
|
onAssign: (payload) => {
|
|
4290
4391
|
log(`[realtime] Agent ${payload.agent_id} assigned \u2014 will pick up next cycle`);
|
|
4392
|
+
markAgentForFreshMemorySync(payload.agent_id);
|
|
4291
4393
|
},
|
|
4292
4394
|
onUnassign: (payload) => {
|
|
4293
4395
|
log(`[realtime] Agent ${payload.agent_id} unassigned`);
|
|
@@ -5374,8 +5476,9 @@ async function processClaudePairSessions(agents) {
|
|
|
5374
5476
|
submitClaudePairCode,
|
|
5375
5477
|
spawnPairSession,
|
|
5376
5478
|
killPairSession,
|
|
5377
|
-
pairTmuxSession
|
|
5378
|
-
|
|
5479
|
+
pairTmuxSession,
|
|
5480
|
+
finalizeClaudePairOnboarding
|
|
5481
|
+
} = await import("../claude-pair-runtime-Q7PNH3ZK.js");
|
|
5379
5482
|
for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
|
|
5380
5483
|
log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
|
|
5381
5484
|
const killed = await killPairSession(pairTmuxSession(pairId));
|
|
@@ -5450,6 +5553,10 @@ async function processClaudePairSessions(agents) {
|
|
|
5450
5553
|
code: session.code
|
|
5451
5554
|
});
|
|
5452
5555
|
if (result.kind === "success") {
|
|
5556
|
+
const finalize = await finalizeClaudePairOnboarding(pairSession, log);
|
|
5557
|
+
if (!finalize.finalized) {
|
|
5558
|
+
log(`[claude-pair] WARN: ~/.claude.json was not updated during onboarding (pair ${session.pair_id.slice(0, 8)}) \u2014 first agent launch may show the login picker`);
|
|
5559
|
+
}
|
|
5453
5560
|
await reportAndCleanup(session.pair_id, { status: "success" });
|
|
5454
5561
|
} else if (result.kind === "failure") {
|
|
5455
5562
|
await reportAndCleanup(session.pair_id, {
|
|
@@ -5616,6 +5723,13 @@ function generateArtifacts(agent, refreshData, adapter) {
|
|
|
5616
5723
|
var memoryFileHashes = /* @__PURE__ */ new Map();
|
|
5617
5724
|
var lastDownloadHash = /* @__PURE__ */ new Map();
|
|
5618
5725
|
var lastLocalFileHash = /* @__PURE__ */ new Map();
|
|
5726
|
+
var pendingFreshMemorySync = /* @__PURE__ */ new Set();
|
|
5727
|
+
function markAgentForFreshMemorySync(agentId) {
|
|
5728
|
+
pendingFreshMemorySync.add(agentId);
|
|
5729
|
+
memoryFileHashes.delete(agentId);
|
|
5730
|
+
lastDownloadHash.delete(agentId);
|
|
5731
|
+
lastLocalFileHash.delete(agentId);
|
|
5732
|
+
}
|
|
5619
5733
|
function parseMemoryFile(raw, fallbackName) {
|
|
5620
5734
|
const trimmed = raw.trim();
|
|
5621
5735
|
if (!trimmed) return null;
|
|
@@ -5642,6 +5756,16 @@ function parseMemoryFile(raw, fallbackName) {
|
|
|
5642
5756
|
async function syncMemories(agent, configDir, log2) {
|
|
5643
5757
|
const projectDir = join3(configDir, agent.code_name, "project");
|
|
5644
5758
|
const memoryDir = join3(projectDir, "memory");
|
|
5759
|
+
const isFreshSync = pendingFreshMemorySync.has(agent.agent_id);
|
|
5760
|
+
if (isFreshSync) {
|
|
5761
|
+
log2(`[memory-sync] Fresh-sync requested for '${agent.code_name}' \u2014 pulling DB first`);
|
|
5762
|
+
const ok = await downloadMemories(agent, memoryDir, log2, { force: true });
|
|
5763
|
+
if (!ok) {
|
|
5764
|
+
log2(`[memory-sync] Fresh-sync download failed for '${agent.code_name}' \u2014 skipping upload, will retry next tick`);
|
|
5765
|
+
return;
|
|
5766
|
+
}
|
|
5767
|
+
pendingFreshMemorySync.delete(agent.agent_id);
|
|
5768
|
+
}
|
|
5645
5769
|
if (existsSync2(memoryDir)) {
|
|
5646
5770
|
const prevHashes = memoryFileHashes.get(agent.agent_id) ?? /* @__PURE__ */ new Map();
|
|
5647
5771
|
const currentHashes = /* @__PURE__ */ new Map();
|
|
@@ -5682,6 +5806,11 @@ async function syncMemories(agent, configDir, log2) {
|
|
|
5682
5806
|
}
|
|
5683
5807
|
}
|
|
5684
5808
|
}
|
|
5809
|
+
if (!isFreshSync) {
|
|
5810
|
+
await downloadMemories(agent, memoryDir, log2, { force: false });
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
async function downloadMemories(agent, memoryDir, log2, { force }) {
|
|
5685
5814
|
const localFiles = existsSync2(memoryDir) ? readdirSync2(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
|
|
5686
5815
|
const localListHash = createHash("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
|
|
5687
5816
|
const prevLocalHash = lastLocalFileHash.get(agent.agent_id);
|
|
@@ -5691,20 +5820,21 @@ async function syncMemories(agent, configDir, log2) {
|
|
|
5691
5820
|
agent_id: agent.agent_id
|
|
5692
5821
|
});
|
|
5693
5822
|
const responseHash = createHash("sha256").update(JSON.stringify(dbMemories.memories ?? [])).digest("hex").slice(0, 16);
|
|
5694
|
-
if (prevDownload && prevLocalHash === localListHash && lastDownloadHash.get(agent.agent_id) === responseHash) {
|
|
5695
|
-
return;
|
|
5823
|
+
if (!force && prevDownload && prevLocalHash === localListHash && lastDownloadHash.get(agent.agent_id) === responseHash) {
|
|
5824
|
+
return true;
|
|
5696
5825
|
}
|
|
5697
5826
|
lastDownloadHash.set(agent.agent_id, responseHash);
|
|
5698
5827
|
lastLocalFileHash.set(agent.agent_id, localListHash);
|
|
5699
5828
|
if (dbMemories.memories?.length) {
|
|
5700
5829
|
mkdirSync2(memoryDir, { recursive: true });
|
|
5701
|
-
const existingFileSet = new Set(localFiles.map((f) => f.replace(/\.md$/, "").toLowerCase()));
|
|
5702
5830
|
let written = 0;
|
|
5703
|
-
|
|
5831
|
+
let overwritten = 0;
|
|
5832
|
+
for (let i = 0; i < dbMemories.memories.length; i++) {
|
|
5833
|
+
const mem = dbMemories.memories[i];
|
|
5704
5834
|
const rawSlug = mem.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60);
|
|
5705
|
-
const slug = rawSlug || `memory-${
|
|
5706
|
-
|
|
5707
|
-
const
|
|
5835
|
+
const slug = rawSlug || `memory-${i}`;
|
|
5836
|
+
const filePath = join3(memoryDir, `${slug}.md`);
|
|
5837
|
+
const desired = `---
|
|
5708
5838
|
name: ${JSON.stringify(mem.name)}
|
|
5709
5839
|
type: ${mem.type}
|
|
5710
5840
|
description: ${JSON.stringify(mem.content.slice(0, 200))}
|
|
@@ -5712,17 +5842,30 @@ description: ${JSON.stringify(mem.content.slice(0, 200))}
|
|
|
5712
5842
|
|
|
5713
5843
|
${mem.content}
|
|
5714
5844
|
`;
|
|
5715
|
-
|
|
5716
|
-
|
|
5845
|
+
if (existsSync2(filePath)) {
|
|
5846
|
+
let existing = "";
|
|
5847
|
+
try {
|
|
5848
|
+
existing = readFileSync2(filePath, "utf-8");
|
|
5849
|
+
} catch {
|
|
5850
|
+
}
|
|
5851
|
+
if (existing === desired) continue;
|
|
5852
|
+
writeFileSync2(filePath, desired);
|
|
5853
|
+
overwritten++;
|
|
5854
|
+
} else {
|
|
5855
|
+
writeFileSync2(filePath, desired);
|
|
5856
|
+
written++;
|
|
5857
|
+
}
|
|
5717
5858
|
}
|
|
5718
|
-
if (written > 0) {
|
|
5859
|
+
if (written > 0 || overwritten > 0) {
|
|
5719
5860
|
const updatedFiles = readdirSync2(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
5720
5861
|
lastLocalFileHash.set(agent.agent_id, createHash("sha256").update(updatedFiles.join(",")).digest("hex").slice(0, 16));
|
|
5721
|
-
log2(`
|
|
5862
|
+
log2(`Memory download for '${agent.code_name}': wrote ${written} new, overwrote ${overwritten} stale`);
|
|
5722
5863
|
}
|
|
5723
5864
|
}
|
|
5865
|
+
return true;
|
|
5724
5866
|
} catch (err) {
|
|
5725
5867
|
log2(`Memory download failed for '${agent.code_name}': ${err.message}`);
|
|
5868
|
+
return false;
|
|
5726
5869
|
}
|
|
5727
5870
|
}
|
|
5728
5871
|
async function cleanupAgentFiles(codeName, agentDir) {
|
|
@@ -6056,6 +6199,7 @@ process.on("disconnect", () => {
|
|
|
6056
6199
|
});
|
|
6057
6200
|
export {
|
|
6058
6201
|
ChildProcessError,
|
|
6202
|
+
markAgentForFreshMemorySync,
|
|
6059
6203
|
startManager,
|
|
6060
6204
|
stopManager
|
|
6061
6205
|
};
|