@integrity-labs/agt-cli 0.28.1049 → 0.28.1050
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 +6 -6
- package/dist/{chunk-MVBTIU7A.js → chunk-FNUTMHWV.js} +3 -3
- package/dist/{chunk-PLCZUMFX.js → chunk-GQYD3ESJ.js} +5 -5
- package/dist/{chunk-PLCZUMFX.js.map → chunk-GQYD3ESJ.js.map} +1 -1
- package/dist/{chunk-SFOIRACS.js → chunk-MQ4COFCI.js} +5 -5
- package/dist/chunk-MQ4COFCI.js.map +1 -0
- package/dist/{chunk-TQDIIS3X.js → chunk-OCMJ5E6J.js} +36 -2
- package/dist/{chunk-TQDIIS3X.js.map → chunk-OCMJ5E6J.js.map} +1 -1
- package/dist/{claude-pair-runtime-XFVA6KHM.js → claude-pair-runtime-53UOTF6V.js} +3 -3
- package/dist/lib/manager-worker.js +15 -15
- package/dist/{persistent-session-FTAHFPQK.js → persistent-session-3FT727PQ.js} +4 -4
- package/dist/{responsiveness-probe-QIBWAB4J.js → responsiveness-probe-DEG3SWG3.js} +4 -4
- package/dist/{session-auth-dead-6LW2TJTG.js → session-auth-dead-VFRUHVTF.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-SFOIRACS.js.map +0 -1
- /package/dist/{chunk-MVBTIU7A.js.map → chunk-FNUTMHWV.js.map} +0 -0
- /package/dist/{claude-pair-runtime-XFVA6KHM.js.map → claude-pair-runtime-53UOTF6V.js.map} +0 -0
- /package/dist/{persistent-session-FTAHFPQK.js.map → persistent-session-3FT727PQ.js.map} +0 -0
- /package/dist/{responsiveness-probe-QIBWAB4J.js.map → responsiveness-probe-DEG3SWG3.js.map} +0 -0
- /package/dist/{session-auth-dead-6LW2TJTG.js.map → session-auth-dead-VFRUHVTF.js.map} +0 -0
|
@@ -281,14 +281,44 @@ function focusedConfirmDialogBlock(screen) {
|
|
|
281
281
|
const lines = screen.split("\n");
|
|
282
282
|
let footer = -1;
|
|
283
283
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
284
|
-
|
|
284
|
+
const line = lines[i];
|
|
285
|
+
if (line.includes("Enter to confirm")) {
|
|
285
286
|
footer = i;
|
|
286
287
|
break;
|
|
287
288
|
}
|
|
289
|
+
if (isToolPermissionFooter(line)) {
|
|
290
|
+
const block = lines.slice(Math.max(0, i - USAGE_LIMIT_BLOCK_LINES), i + 1);
|
|
291
|
+
const inputBoxAfter = lines.slice(i + 1).some((l) => INPUT_BOX_RULE.test(l.trim()));
|
|
292
|
+
if (!inputBoxAfter && block.some((l) => NUMBERED_OPTION_ROW.test(l))) {
|
|
293
|
+
footer = i;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
288
297
|
}
|
|
289
298
|
if (footer < 0) return null;
|
|
290
299
|
return lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer + 1);
|
|
291
300
|
}
|
|
301
|
+
function isToolPermissionFooter(line) {
|
|
302
|
+
return /^\s*Esc to cancel · Tab to amend\b/.test(line);
|
|
303
|
+
}
|
|
304
|
+
var INPUT_BOX_RULE = /^[─━]{10,}/;
|
|
305
|
+
function isParkedOnToolPermissionPrompt(screen) {
|
|
306
|
+
const block = focusedConfirmDialogBlock(screen);
|
|
307
|
+
if (!block) return false;
|
|
308
|
+
const footerLine = block[block.length - 1];
|
|
309
|
+
if (!isToolPermissionFooter(footerLine)) return false;
|
|
310
|
+
const lines = screen.split("\n");
|
|
311
|
+
let footerIdx = -1;
|
|
312
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
313
|
+
if (lines[i] === footerLine) {
|
|
314
|
+
footerIdx = i;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (footerIdx < 0) return false;
|
|
319
|
+
return !lines.slice(footerIdx + 1).some((l) => INPUT_BOX_RULE.test(l.trim()));
|
|
320
|
+
}
|
|
321
|
+
var NUMBERED_OPTION_ROW = /^\s*(?:❯\s*)?\d\.\s+\S/;
|
|
292
322
|
function isUnclaimedConfirmDialog(screen) {
|
|
293
323
|
if (focusedConfirmDialogBlock(screen) === null) return false;
|
|
294
324
|
if (sweepDialogs(screen) !== null) return false;
|
|
@@ -315,6 +345,10 @@ function parkedModalKind(screen) {
|
|
|
315
345
|
if (isUnclaimedConfirmDialog(text)) return "unclaimed-modal";
|
|
316
346
|
return null;
|
|
317
347
|
}
|
|
348
|
+
if (isParkedOnToolPermissionPrompt(screen)) {
|
|
349
|
+
if (sweepDialogs(screen) !== null) return null;
|
|
350
|
+
return "unclaimed-modal";
|
|
351
|
+
}
|
|
318
352
|
if (/Claude account with subscription|Anthropic Console account|3rd-party platform/.test(last)) {
|
|
319
353
|
return isLoginPickerVisible(screen) ? "login-picker" : null;
|
|
320
354
|
}
|
|
@@ -352,4 +386,4 @@ export {
|
|
|
352
386
|
classifyUnanswerablePane,
|
|
353
387
|
summarizeUnanswerablePane
|
|
354
388
|
};
|
|
355
|
-
//# sourceMappingURL=chunk-
|
|
389
|
+
//# sourceMappingURL=chunk-OCMJ5E6J.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/claude-dialogs.ts"],"sourcesContent":["/**\n * ENG-6017: shared Claude Code TUI dialog detection + dismissal.\n *\n * Extracted from persistent-session.ts's acceptDialogs() cascade so the\n * dialog knowledge has exactly one home, consumable from:\n *\n * - `acceptDialogs()` in persistent-session.ts — the spawn-time loop that\n * walks an agent through the first-run dialog cascade (theme → trust →\n * MCP → bypass …).\n * - `channel-input-watchdog.ts` — the per-poll-cycle watchdog that fires\n * Enter at stuck channel input. Before ENG-6017 the watchdog was\n * dialog-blind: when Claude Code's session-feedback dialog (\"How is\n * Claude doing this session?\") overlaid the pane, the watchdog's\n * single-shot Enter went into the dialog instead of the input box and\n * the inbound message sat unsubmitted for 40+ minutes (koda,\n * 2026-06-04) while every health metric stayed green.\n * - `injectMessageWithStatus()` — pre-send pane hygiene on the tmux\n * send-keys fallback path.\n *\n * Living in its own module (rather than persistent-session.ts) breaks the\n * import cycle persistent-session → channel-input-watchdog →\n * persistent-session that a shared export would otherwise create.\n *\n * DEFAULT-DENY: `sweepDialogs()` only ever returns an action for an\n * explicitly recognised dialog. An unknown dialog gets `null` — never a\n * blind Enter — because a future dialog's default option could be\n * destructive. Callers that want visibility into unknown overlays should\n * log the pane themselves.\n */\n\nimport { execFileSync } from 'node:child_process';\n\n/**\n * A recognised dialog plus the keystrokes that dismiss it.\n *\n * `keys` are tmux key names sent as individual `tmux send-keys` calls —\n * never batched into one call, so a multi-byte sequence can't get wrapped\n * into a single bracketed paste (the CSI-u trap documented on\n * defaultArmSender in persistent-session.ts).\n */\nexport interface DialogAction {\n kind:\n | 'theme-picker'\n | 'folder-trust'\n | 'resume-mode'\n | 'dev-channels'\n | 'mcp-servers'\n | 'bypass-permissions'\n | 'session-feedback'\n | 'usage-limit-choice'\n | 'consumer-terms';\n /** tmux key names, one send-keys invocation each. */\n keys: readonly string[];\n /** Delay between consecutive key sends (selector dialogs need a beat\n * between picking an option and confirming it). */\n interKeyDelayMs: number;\n /** Past-tense log fragment, e.g. \"Auto-accepted theme picker\". Callers\n * append their own context (`for '<codeName>'` etc.). */\n logMessage: string;\n}\n\n/**\n * Detect whether Claude Code is showing the **login picker** dialog.\n *\n * ENG-4634: this dialog appears when ~/.claude.json is missing or\n * Claude Code can't validate the saved session. Pressing Enter on\n * the default (1. Claude account with subscription) kicks off a\n * browser-based OAuth flow that an unattended agent can't complete —\n * the helper used to fall through to the generic `❯ no Enter to\n * confirm` exit branch and declare the session \"ready\" while the\n * actual claude REPL was still on the picker. Without explicit\n * detection, every manager respawn would silently flip the agent\n * back to the picker and never recover.\n *\n * Pattern matches the literal option strings claude renders. Both\n * 'Claude account with subscription' and 'Anthropic Console account'\n * are present on the picker (and not in the post-login UI), so the\n * conjunction is unambiguous.\n *\n * NOT part of sweepDialogs() — the picker must never be keyed past\n * (it needs an operator OAuth); acceptDialogs() handles it specially.\n */\nexport function isLoginPickerVisible(screen: string): boolean {\n return (\n screen.includes('Select login method') ||\n (screen.includes('Claude account with subscription') &&\n screen.includes('Anthropic Console account'))\n );\n}\n\n/**\n * ENG-10619: the row we ALWAYS pick on Claude Code's consumer-terms dialog.\n *\n * Updates to Consumer Terms and Policies\n * …\n * ❯ 1. Accept terms · Help improve our AI models: ON\n * 2. Accept terms · Help improve our AI models: OFF\n *\n * Enter to confirm · Esc to cancel\n *\n * The cursor defaults to ON, which opts the account's chats and coding\n * sessions into model training and extends retention from 30 days to 5\n * years. A bare Enter would therefore consent on the customer's behalf, so\n * this row is selected by NAME (selectRowKeys) and never by position or\n * default. The `·` is U+00B7, the same separator Claude Code uses in its\n * confirm footer.\n */\nexport const CONSUMER_TERMS_OFF_OPTION =\n 'Accept terms \\u00b7 Help improve our AI models: OFF';\n\n/**\n * ENG-10619: the consumer-terms dialog is on screen. Keyed on the\n * conjunction of the heading and the OFF row's own text so prose that\n * mentions either alone does not match; selectRowKeys still requires a\n * focused modal and a complete option row before anything is keyed.\n */\nexport function isConsumerTermsDialogVisible(screen: string): boolean {\n return (\n screen.includes('Updates to Consumer Terms') &&\n screen.includes(CONSUMER_TERMS_OFF_OPTION)\n );\n}\n\n/**\n * ENG-10619: keys that pick \"Help improve our AI models: OFF\" on a focused\n * consumer-terms dialog, or null (answer nothing) when that row cannot be\n * resolved. Shared by sweepDialogs() and the Claude pair/re-login flow,\n * which drives its own dialog loop.\n */\nexport function consumerTermsOffKeys(screen: string): readonly string[] | null {\n if (!isConsumerTermsDialogVisible(screen)) return null;\n return selectRowKeys(screen, CONSUMER_TERMS_OFF_OPTION);\n}\n\n/**\n * ENG-10972: the operator-facing remedy for a session a human has to unstick at the\n * pane. EVERY \"needs a human\" line goes through this, and that is the point.\n *\n * On 2026-09-24 three Primo Cards agents sat blocked on the rate-limit prompt with\n * the consumer-terms dialog ALSO on the pane. The remedy text said only \"tmux\n * attach\". That dialog's cursor defaults to \"Help improve our AI models: ON\", which\n * opts the CUSTOMER's account into model training and stretches retention from 30\n * days to 5 years. The operator who cleared them by hand was one Enter away from\n * consenting on a customer's behalf, three times, and nothing he was shown said so.\n *\n * The blocking KIND never says \"consumer-terms\": on those panes the blocker was the\n * rate-limit prompt, and the consent dialog sat beside it. So the caller passes\n * whether a consent dialog is VISIBLE on the pane. That is a boolean, which is not\n * pane content, so it may leave the host in an alert row. The pane text itself may\n * not (see summarizeUnanswerablePane).\n *\n * Three states, because one site speaks before the pane has been read:\n * true -> name the safe row, and say not to press Enter\n * false -> the bare attach command; no warning on every hold, or it becomes noise\n * null -> we did not look, so a CONDITIONAL caution rather than silence\n *\n * The named row is `CONSUMER_TERMS_OFF_OPTION`, the same constant the automated\n * sweeper selects by name, so the human and the machine give the same answer.\n */\nexport function paneRemedy(codeName: string, consentDialogVisible: boolean | null): string {\n const attach = `tmux attach -t agt-${codeName}`;\n if (consentDialogVisible === true) {\n return (\n // Never glue punctuation to the command: `${attach}.` is copied as a session\n // named \"agt-<name>.\" that does not exist. Same \" (...)\" shape as the null\n // branch, and single-line because every caller embeds this mid-sentence.\n `${attach} (A CONSENT DIALOG IS ON THIS PANE: do NOT press Enter - the cursor defaults ` +\n `to opting this account into model training. Select \"${CONSUMER_TERMS_OFF_OPTION}\" by name, or Esc)`\n );\n }\n if (consentDialogVisible === null) {\n return (\n `${attach} (if the Consumer Terms dialog is showing, do NOT press Enter on the default - ` +\n `select \"${CONSUMER_TERMS_OFF_OPTION}\" by name)`\n );\n }\n return attach;\n}\n\n/**\n * Detect Claude Code's resume-mode dialog (ENG-5364).\n *\n * On `claude --resume <uuid>` against a transcript large enough to\n * trigger Claude Code 2.1.x's context-management heuristic, the agent\n * lands on an interactive picker offering:\n *\n * ❯ 1. Resume from summary (recommended)\n * 2. Resume full session as-is\n * 3. Don't ask me again\n *\n * Without auto-dismissal the agent sits silently waiting for keyboard\n * input on every manager respawn — channel inbounds stack up while\n * health metrics stay green. Surfaced fleet-wide on 2026-05-20\n * (don/stirling/maven hit it on a single manager restart).\n *\n * Match on the conjunction of two distinct option strings so a\n * passing mention of \"Resume\" in a transcript doesn't false-positive.\n */\nexport function isResumeModeDialogVisible(screen: string): boolean {\n return (\n screen.includes('Resume from summary') &&\n screen.includes(\"Don't ask me again\")\n );\n}\n\n/**\n * Detect Claude Code's session-feedback dialog (ENG-6017).\n *\n * After some turns Claude Code renders an optional rating prompt:\n *\n * ● How is Claude doing this session? (optional)\n * 1: Bad 2: Fine 3: Good 0: Dismiss\n *\n * It waits for a digit — Enter does nothing — so any injected message\n * sits in the input box unsubmitted, and the channel-input-watchdog's\n * Enter is swallowed too. Observed live on koda (agt-aws-1) 2026-06-04:\n * an operator Slack DM sat typed-but-unsubmitted for 40+ minutes behind\n * this dialog while pane-activity / synthetic-probe / heartbeat all\n * stayed green.\n *\n * Match the question text together with the literal `0: Dismiss` option\n * so a transcript merely *quoting* the question doesn't false-positive.\n */\nexport function isSessionFeedbackDialogVisible(screen: string): boolean {\n return (\n screen.includes('How is Claude doing this session') &&\n screen.includes('0: Dismiss')\n );\n}\n\n/**\n * The stop-the-session safe option (ENG-8213). Exported so tests and callers\n * assert against the same literal the matcher uses, rather than a copy that\n * could drift.\n *\n * Kept as the name it has always had because callers and tests reference it,\n * but it is no longer the ONLY option this module selects — see\n * `USAGE_LIMIT_SAFE_OPTIONS` for the full preference order.\n */\nexport const USAGE_LIMIT_SAFE_OPTION = 'Stop and wait for limit to reset';\n\n/**\n * The auto-continue option, added by Claude Code on the Team seat spend-limit\n * variant of this modal:\n *\n * 2. Wait here, then continue automatically at Aug 21, 10am\n *\n * A PREFIX, deliberately: the row carries a reset date that changes every\n * time, so the literal can only ever be the stable leading text.\n */\nexport const USAGE_LIMIT_CONTINUE_OPTION = 'Wait here, then continue automatically';\n\n/**\n * Options this module is willing to press, MOST PREFERRED FIRST.\n *\n * `Wait here, then continue automatically` outranks `Stop and wait for limit to\n * reset` because the two differ in exactly the way that matters to an\n * unattended agent: both idle until the cap lifts, but only the first RESUMES\n * by itself afterwards. \"Stop and wait\" leaves the session parked until a human\n * touches it, which is the state this whole handler exists to avoid — we would\n * have answered the modal and still needed the human.\n *\n * Neither spends money, which is the invariant that governs this list. Every\n * other row on either variant of this modal is excluded on purpose:\n *\n * - `Switch to usage credits` — starts pay-as-you-go spend.\n * - `Switch to Team plan` — a plan change.\n * - `Ask your admin for more usage` — spends nothing, but it pages a human\n * and asks them to raise a budget. That is a decision with a cost attached\n * and it is not ours to make automatically; a human choosing to raise the\n * cap is a different act from a fleet of agents asking them to.\n */\nexport const USAGE_LIMIT_SAFE_OPTIONS: readonly string[] = [\n USAGE_LIMIT_CONTINUE_OPTION,\n USAGE_LIMIT_SAFE_OPTION,\n];\n\n/**\n * Every option label known to appear on this modal, including the ones we would\n * never press. This is the RECOGNITION vocabulary, and it is deliberately wider\n * than the selection vocabulary above.\n *\n * ENG-8213 anchored recognition on the BILLING rows alone, which tied \"is this\n * the usage-limit modal?\" to options that only exist on the subscription\n * variant. Claude Code's Team seat spend-limit variant renders\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Wait here, then continue automatically at Aug 21, 10am\n * 3. Ask your admin for more usage\n *\n * with no billing row at all, so the modal was not recognised — and the failure\n * was not a no-op. This pane renders \"Enter to confirm\", and the `mcp-servers`\n * branch below fires on `'Enter to confirm' && 'MCP'` with \"MCP\" trivially\n * present in any agent's scrollback. So the unrecognised variant fell through to\n * a BARE ENTER, confirming whichever row the cursor sat on — row 1, \"Stop and\n * wait\", the one option that guarantees a human is needed. Observed on\n * acquire-intelligence, 2026-08-19.\n *\n * Recognising by \"an option row this modal is known to render\" rather than by\n * billing vocabulary keeps the structural guard (it must still be a numbered\n * ROW inside the block adjacent to the nearest footer) while covering both\n * variants and any future one whose rows we add here.\n */\nconst USAGE_LIMIT_KNOWN_OPTIONS: readonly string[] = [\n ...USAGE_LIMIT_SAFE_OPTIONS,\n 'Switch to usage credits',\n 'Switch to Team plan',\n // ENG-9006: the two-option variant Claude Code shipped ~2026-08-17. Filed\n // with a wedged pane and four unanswered customer Telegram messages queued\n // behind it.\n 'Upgrade your plan',\n 'Ask your admin for more usage',\n];\n\n/**\n * Detect Claude Code's usage-limit choice dialog (ENG-8213).\n *\n * On hitting the plan limit, Claude Code blocks the TUI on:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Switch to usage credits\n * 3. Switch to Team plan\n *\n * Enter to confirm · Esc to cancel\n *\n * Nothing answered it, so the session sat on the modal and the agent was\n * unavailable until a human attached to the pane.\n *\n * Matched STRUCTURALLY: a KNOWN option (USAGE_LIMIT_KNOWN_OPTIONS) must appear\n * as a numbered option ROW (`<indent>[❯ ]<digit>. <text>`), not merely\n * somewhere in the capture, and the modal's confirm affordance must be present.\n *\n * The anchor was originally the BILLING rows specifically, which silently made\n * recognition subscription-only and let the Team seat spend-limit variant fall\n * through to a bare Enter. See USAGE_LIMIT_KNOWN_OPTIONS. The structural\n * requirement below is unchanged and is what actually does the safety work; only\n * the vocabulary widened.\n *\n * The looser \"billing phrase anywhere && 'Enter to confirm'\" version was\n * actively dangerous, and in the opposite direction to the bug this fixes.\n * \"Enter to confirm\" is rendered by OTHER dialogs (the MCP confirm among\n * them), and an agent that merely writes the words \"Switch to usage credits\"\n * - discussing a capped teammate, quoting this very file - leaves them in the\n * scrollback. Both true at once and this predicate fires on a pane that is\n * not this dialog. Because sweepDialogs() checks it FIRST and then finds no\n * safe option row, it would return null AND isUnanswerableUsageLimitDialog()\n * would tell every caller to hold the pane: the real dialog never gets\n * answered and the agent wedges. That is the ENG-8194 self-gating shape with\n * a worse blast radius, so the match has to key on layout, not vocabulary.\n */\n/**\n * How far above the \"Enter to confirm\" footer an option row may sit and still\n * count as part of the SAME modal.\n *\n * The real dialog puts its furthest option 4 lines above the footer, so 6\n * carries the whole block with room for a wrap. Deliberately tight: every line\n * of slack here is scrollback that a numbered write-up could occupy, and the\n * cost of being too generous (wedging the pane on a false positive) is worse\n * than the cost of being too tight (failing to answer, which is the status quo\n * this handler improves on).\n */\nconst USAGE_LIMIT_BLOCK_LINES = 6;\n\nfunction optionRowDigit(line: string, label: string): string | null {\n const m = line.match(\n new RegExp(\n String.raw`^[^\\S\\n]*(?:❯[^\\S\\n]*)?(\\d)\\.[^\\S\\n]*` +\n label.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`),\n ),\n );\n return m?.[1] ?? null;\n}\n\n/**\n * Return the modal's own lines, or null when this dialog is not on screen.\n *\n * The match is bound to a REGION, not to the capture as a whole: a known\n * option row and the confirm footer must belong to the same block. Two earlier\n * versions of this predicate were too loose, in the same direction each time:\n *\n * 1. \"billing phrase anywhere && 'Enter to confirm' anywhere\" - satisfied by\n * prose about a capped teammate sitting behind an unrelated dialog.\n * 2. \"billing option ROW anywhere && 'Enter to confirm' anywhere\" - still\n * satisfied by a numbered list in prose (`1. Switch to Team plan`, which\n * is exactly how an agent writes up this very ticket) plus a confirm\n * footer from a different dialog.\n *\n * Both misfire the same way, and it is the dangerous way: sweepDialogs() checks\n * this branch FIRST (so the mcp-servers branch cannot bare-Enter the real\n * modal), so a false positive with no safe option row returns null AND makes\n * isUnanswerableUsageLimitDialog() tell every caller to hold the pane. The\n * dialog actually on screen never gets answered and the agent wedges.\n */\n/**\n * The lines of the FOCUSED modal — the block immediately above the nearest\n * \"Enter to confirm\" footer — or null when no modal is focused.\n *\n * Extracted from usageLimitModalBlock so every structural predicate reads the\n * same region by the same rule. \"Nearest footer\" is load-bearing and was\n * arrived at the hard way: with a newer dialog on screen and an older one still\n * in the scrollback, a backwards search skips the live dialog's footer and\n * matches the STALE one, so we would classify the pane as a dialog that is not\n * focused and key whatever actually is.\n */\nfunction focusedModalBlock(screen: string, blockLines: number): string[] | null {\n const lines = screen.split('\\n');\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i]!.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n }\n if (footer < 0) return null;\n return lines.slice(Math.max(0, footer - blockLines), footer);\n}\n\n/**\n * The MCP-servers dialog's own option rows.\n *\n * New MCP servers found in .mcp.json\n * ❯ 1. Use this and all future MCP servers in this project\n * 2. Continue without using these MCP servers\n *\n * Enter to confirm · Esc to cancel\n */\nconst MCP_DIALOG_OPTIONS: readonly string[] = [\n 'Use this and all future MCP servers in this project',\n 'Continue without using these MCP servers',\n];\n\n/**\n * Detect the MCP-servers dialog STRUCTURALLY.\n *\n * This branch used to read `screen.includes('Enter to confirm') &&\n * screen.includes('MCP')`, which is not a detector — it is a catch-all. Every\n * agent pane carries the string \"MCP\" somewhere (`Connected to MCP server:\n * slack` is printed at startup and never scrolls out of a quiet pane), so the\n * condition reduced to \"any focused confirm dialog at all\", and the action is a\n * BARE ENTER on whatever row the cursor sits on.\n *\n * That is not a hypothetical. It is the mechanism behind the acquire-intelligence\n * wedge on 2026-08-19: the spend-limit modal was not recognised by the\n * usage-limit branch, fell to this one, and got an Enter on the pre-selected\n * \"Stop and wait for limit to reset\". ENG-8213's fix for the SAME hazard was to\n * order the usage-limit branch first — correct, and only a fix for the one\n * dialog anybody had thought of.\n *\n * It also silently defeats the ENG-9006 backstop: an unclaimed modal cannot be\n * reported as unclaimed if this branch claims everything. Tightening here is\n * what lets an unrecognised dialog reach `isUnclaimedConfirmDialog` and become\n * visible instead of being blindly confirmed.\n */\nexport function isMcpServersDialogVisible(screen: string): boolean {\n const block = focusedModalBlock(screen, USAGE_LIMIT_BLOCK_LINES);\n if (!block) return false;\n return block.some((l) => MCP_DIALOG_OPTIONS.some((label) => optionRowDigit(l, label) !== null));\n}\n\n/**\n * ENG-9224 — the SAME dialog after Claude Code 2.1.237 replaced the widget.\n *\n * `isMcpServersDialogVisible` above matches a numbered option list. 2.1.237\n * ships a checkbox multi-select instead, with neither the digits nor either of\n * the `MCP_DIALOG_OPTIONS` labels:\n *\n * 3 new MCP servers found in this project\n * Select any you wish to enable.\n *\n * MCP servers may execute code or access system resources. All tool calls\n * require approval. Learn more in the MCP documentation.\n *\n * ❯ [✓] augmented\n * [✓] direct-chat\n * [✓] composio_googlesheets\n * Space to select · Enter to confirm · Esc to reject all\n *\n * So the branch stopped firing, agents froze on it, and the presence reaper\n * restarted them into the identical prompt until the breaker paused them. Both\n * customer orgs onboarded on 2026-08-20 died this way.\n *\n * This is the second rewrite of this dialog we have chased: ENG-5375 moved the\n * match onto the option TEXT when 2.1.146 reworded the prompt. Matching on text\n * is what left us exposed when the widget itself changed, so this one keys on\n * STRUCTURE — a header, a checked row, a confirm footer — which survives\n * renaming the servers or reordering the list.\n *\n * ## Why a checked row and not just any row\n *\n * `Enter` accepts the CURRENT selection. If Claude Code ever renders this with\n * nothing pre-selected, confirming it enables nothing: the servers stay unbound,\n * the reaper loop continues, and the log says `Auto-accepted MCP servers` while\n * no progress was made. That is strictly worse than today, because it is\n * invisible.\n *\n * So an all-unchecked dialog is deliberately NOT claimed. It falls through to\n * `isUnclaimedConfirmDialog` and becomes a human's problem — the correct outcome\n * for a state we cannot safely resolve by pressing one key.\n */\nconst MCP_CHECKBOX_HEADER = /new MCP servers? found in this project/i;\n\n/**\n * A CHECKED checkbox row: `❯ [✓] augmented`, ` [x] direct-chat`.\n *\n * `[ ]` is excluded on purpose — see the \"why a checked row\" note above. The\n * optional `❯` mirrors `optionRowDigit`'s handling of the focus caret.\n */\nconst MCP_CHECKED_ROW = /^[^\\S\\n]*(?:❯[^\\S\\n]*)?\\[[✓x]\\][^\\S\\n]*\\S/iu;\n\n/**\n * Wider than `USAGE_LIMIT_BLOCK_LINES` because this dialog puts its header\n * above two lines of prose, a blank line, and one row PER SERVER. Six lines\n * reaches the checkboxes but not the header. Twenty covers a comfortable\n * server count while still binding the match to a region rather than the whole\n * capture — the discipline that keeps this file's detectors from becoming the\n * catch-all described above.\n */\nconst MCP_CHECKBOX_BLOCK_LINES = 20;\n\nexport function isMcpCheckboxDialogVisible(screen: string): boolean {\n const block = focusedModalBlock(screen, MCP_CHECKBOX_BLOCK_LINES);\n if (!block) return false;\n if (!block.some((l) => MCP_CHECKBOX_HEADER.test(l))) return false;\n return block.some((l) => MCP_CHECKED_ROW.test(l));\n}\n\nfunction usageLimitModalBlock(screen: string): string[] | null {\n const lines = screen.split('\\n');\n // ONLY the nearest footer. Scanning further back was the fourth version of\n // this bug: with a newer dialog on screen and an older usage-limit block\n // still in the scrollback, a backwards search skips the live dialog's footer\n // (no billing row) and matches the STALE one - so we would classify the pane\n // as the usage-limit modal and send a digit + Enter into whatever dialog is\n // actually focused. The live dialog is always the last footer; treating this\n // as \"search the capture\" rather than \"read the focused dialog\" is what kept\n // producing near-misses.\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i]!.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n }\n if (footer < 0) return null;\n const block = lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer);\n const hasKnownOptionRow = block.some((l) =>\n USAGE_LIMIT_KNOWN_OPTIONS.some((label) => optionRowDigit(l, label) !== null),\n );\n return hasKnownOptionRow ? block : null;\n}\n\n/**\n * Detect Claude Code's usage-limit choice dialog (ENG-8213).\n *\n * On hitting the plan limit, Claude Code blocks the TUI on:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Switch to usage credits\n * 3. Switch to Team plan\n *\n * Enter to confirm · Esc to cancel\n *\n * ...or, on a Claude Team seat that has hit its individual spend limit:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Wait here, then continue automatically at Aug 21, 10am\n * 3. Ask your admin for more usage\n *\n * Enter to confirm · Esc to cancel\n *\n * Nothing answered it, so the session sat on the modal and the agent was\n * unavailable until a human attached to the pane.\n */\nexport function isUsageLimitChoiceDialogVisible(screen: string): boolean {\n return usageLimitModalBlock(screen) !== null;\n}\n\n/**\n * Locate the option to press by READING ITS NUMBER off the line that carries\n * the option's text — never by assuming it is option 1.\n *\n * This is the whole safety design (ENG-8213). Several rows on this modal cost\n * money or a human's attention: \"Switch to usage credits\" starts pay-as-you-go\n * spend, \"Switch to Team plan\" is a plan change, \"Ask your admin for more\n * usage\" pages a person to raise a budget. Selecting by position would mean a\n * future reordering of this menu silently turns an auto-answer into a purchase.\n * Deriving the digit from the matched text means a reorder just changes which\n * digit we send, and an unrecognised menu yields `null` (answer nothing) rather\n * than a guess.\n *\n * Returns the chosen row's digit (as a tmux key name) and the label it matched,\n * or `null` when no selectable option is present — failing to clear a modal is\n * recoverable; buying credits is not.\n */\nexport function findUsageLimitSafeOption(\n screen: string,\n): { key: string; label: string } | null {\n // Searched inside the modal's own block, so a numbered line elsewhere on the\n // pane can never supply the digit we are about to press.\n const block = usageLimitModalBlock(screen);\n if (!block) return null;\n // Preference order comes from USAGE_LIMIT_SAFE_OPTIONS, not from row order:\n // the OUTER loop is the preference list so `Wait here, then continue\n // automatically` wins wherever it sits on the menu. Iterating rows first\n // would make the answer depend on Claude Code's row ordering, which is the\n // select-by-position mistake this function exists to avoid — just one level\n // up.\n for (const label of USAGE_LIMIT_SAFE_OPTIONS) {\n for (const line of block) {\n const digit = optionRowDigit(line, label);\n if (digit !== null) return { key: digit, label };\n }\n }\n return null;\n}\n\n/** The digit alone, as before. */\nexport function findUsageLimitSafeOptionKey(screen: string): string | null {\n return findUsageLimitSafeOption(screen)?.key ?? null;\n}\n\n/**\n * How many option rows a two-choice consent dialog can span. Wider than the\n * rows themselves so the block still reaches the cursor row when Claude Code\n * pads the modal with blank lines (it does, between the prose and the options).\n */\nconst CONSENT_BLOCK_LINES = 12;\n\n/** Index of the LAST element satisfying `pred`, or -1. */\nfunction lastIndexWhere<T>(xs: readonly T[], pred: (x: T) => boolean): number {\n for (let i = xs.length - 1; i >= 0; i--) {\n if (pred(xs[i]!)) return i;\n }\n return -1;\n}\n\n/**\n * True when `line` is a COMPLETE option row for `label` — optionally preceded by\n * the `❯` cursor and/or a `N.` digit, and carrying nothing else.\n *\n * A substring test is not good enough, and the bypass dialog is exactly where\n * that bites. Its detector is `includes('Yes, I accept') && includes('Bypass\n * Permissions')`, and both strings show up in ordinary prose — an agent that\n * reads a manager log, or discusses this very incident, puts them in its own\n * transcript. A substring match would then find the prose line, measure the\n * cursor distance to it, and key that many Downs into whatever dialog is\n * actually focused. Anchoring to the whole row means prose can never be\n * mistaken for a selectable option; at worst we answer nothing.\n *\n * Anchored at both ends deliberately: every label this is called with names a\n * full row ('Yes, I accept', 'Yes, I trust this folder', \"Don't ask me again\"),\n * unlike USAGE_LIMIT_CONTINUE_OPTION, which is a prefix and is matched by\n * findUsageLimitSafeOption's own looser rule.\n */\n/**\n * True when `line` is a menu CURSOR row — the `❯` opens the line (leading\n * whitespace aside) and something follows it.\n *\n * A bare `includes('❯')` is not good enough: the agent's own input prompt\n * renders the same glyph, and so does any prose quoting a menu. Requiring the\n * glyph to START the row is what separates \"this is the selected option\" from\n * \"this line happens to contain an arrow\".\n */\nfunction isCursorRow(line: string): boolean {\n return /^[^\\S\\n]*❯[^\\S\\n]+\\S/.test(line);\n}\n\nfunction isOptionRowFor(line: string, label: string): boolean {\n const escaped = label.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n return new RegExp(\n String.raw`^[^\\S\\n]*(?:❯[^\\S\\n]*)?(?:\\d\\.[^\\S\\n]*)?` +\n escaped +\n String.raw`[^\\S\\n]*$`,\n ).test(line);\n}\n\n/**\n * ENG-9575: keystrokes that select `label` on the FOCUSED modal, for either\n * shape Claude Code renders — or null when the row isn't there.\n *\n * The bug this exists to kill: every branch below used to answer by DIGIT\n * (`['2','Enter']`) or by BARE ENTER, both of which encode an assumption about\n * how the menu is drawn. Claude Code 2.1.250 draws these consent dialogs as an\n * arrow-select list with NO numbered rows, and — this is the part that turns a\n * cosmetic change into an outage — it defaults the cursor to the REFUSING row:\n *\n * ❯ No, exit\n * Yes, I accept\n * Enter to confirm · Esc to cancel\n *\n * A digit is swallowed (verified on 2.1.250: sending `2` does not move the\n * cursor), so `['2','Enter']` confirms whatever is selected — `No, exit`. Claude\n * exits 0, the manager sees a dead session and respawns, and the next spawn\n * lands on the same dialog. On the two hosts provisioned 2026-08-28 that ran\n * 847 and 1,974 times, every agent down from first boot, while the manager\n * logged `Auto-accepted bypass permissions` on each pass.\n *\n * It is self-perpetuating in a way worth naming: Claude records the consent\n * only after a SUCCESSFUL start, so answering it wrongly also prevents the\n * write that would stop it being asked again. One correct answer ends it for\n * good; a host can never get there on its own.\n *\n * Reading the row instead of assuming its position keeps both shapes working\n * and survives a reorder — the same reasoning as findUsageLimitSafeOption,\n * which derives the digit from the matched text rather than trusting row order.\n * Preserves DEFAULT-DENY: an unrecognised menu returns null, so the caller\n * answers nothing rather than keying a row it cannot see.\n */\nexport function selectRowKeys(\n screen: string,\n label: string,\n): readonly string[] | null {\n // FAIL CLOSED without a focused modal, for BOTH shapes. Every one of these\n // dialogs renders `Enter to confirm` (the numbered shape too — see\n // MCP_DIALOG_OPTIONS above), so no footer means no dialog we are entitled to\n // key. The alternative, searching the whole pane, reads lines the agent\n // itself wrote: a transcript listing the options verbatim (\" 2. Yes, I\n // accept\") is a complete option row by any structural test, and answering it\n // types into the REPL.\n const block = focusedModalBlock(screen, CONSENT_BLOCK_LINES);\n if (!block) return null;\n\n // Last match, not first: with an older render of the same dialog still in\n // the block, the live one is the most recent.\n const target = lastIndexWhere(block, (l) => isOptionRowFor(l, label));\n if (target < 0) return null;\n\n // Numbered shape (pre-2.1.250): the row carries its own digit, so press it.\n // Tried first so a host still on an older Claude Code answers as it always\n // has, and so this fix needs no knowledge of which version a host is running.\n const digit = optionRowDigit(block[target]!, label);\n if (digit !== null) return [digit, 'Enter'];\n\n // Arrow-select shape: RELATIVE movement, so we need the menu's own cursor.\n // Only a row whose `❯` STARTS the line is a cursor — prose that merely\n // contains the glyph is not, and neither is the agent's input prompt.\n const cursors: number[] = [];\n for (let i = 0; i < block.length; i++) {\n if (isCursorRow(block[i]!)) cursors.push(i);\n }\n // No cursor means we cannot know what Enter would confirm. Answer nothing:\n // on these dialogs the unknown row is the destructive one, and DEFAULT-DENY\n // is the module's contract.\n if (cursors.length === 0) return null;\n\n // Nearest cursor to the target — option rows are adjacent, so nearest is the\n // menu this row belongs to. A TIE means two candidate menus are equally\n // close and we cannot tell which owns the row; that is ambiguity, and\n // ambiguity is answered with silence rather than a guess.\n let cursor = cursors[0]!;\n let tied = false;\n for (const i of cursors.slice(1)) {\n const d = Math.abs(i - target);\n const best = Math.abs(cursor - target);\n if (d < best) {\n cursor = i;\n tied = false;\n } else if (d === best) {\n tied = true;\n }\n }\n if (tied) return null;\n\n const distance = target - cursor;\n if (distance === 0) return ['Enter'];\n const step = distance > 0 ? 'Down' : 'Up';\n return [...Array<string>(Math.abs(distance)).fill(step), 'Enter'];\n}\n\n/**\n * ENG-9006: the reset moment the modal names, verbatim, or null.\n *\n * The Team seat spend-limit variant renders its auto-continue row as\n *\n * 2. Wait here, then continue automatically at Aug 21, 10am\n *\n * and that tail is the single most authoritative reset string we ever get from\n * Claude Code. Everywhere else we RECONSTRUCT one: the saturated banner often\n * names only an hour (\"resets 1am (UTC)\") and `banner-parser.ts` resolves it to\n * the next occurrence of that hour, which can never be more than 24h out even\n * when the real reset is days away — the ENG-8901 defect, which had operators\n * scheduling against an instant that was three days early.\n *\n * Returned as the RAW STRING on purpose. Parsing it would mean inventing a\n * second date grammar that can drift from the banner parser's, and guessing a\n * timezone the modal does not state. The one consumer is a sentence shown to a\n * human, and \"until Aug 21, 10am\" is exactly as useful to them as a Date would\n * be, while being impossible to get subtly wrong.\n *\n * Bounded so a mis-parse cannot push an arbitrary pane line into a message that\n * reaches a customer.\n */\nexport function findUsageLimitResetHint(screen: string): string | null {\n const block = usageLimitModalBlock(screen);\n if (!block) return null;\n for (const line of block) {\n if (optionRowDigit(line, USAGE_LIMIT_CONTINUE_OPTION) === null) continue;\n const idx = line.indexOf(USAGE_LIMIT_CONTINUE_OPTION);\n const tail = line.slice(idx + USAGE_LIMIT_CONTINUE_OPTION.length).trim();\n // The row reads \"… continue automatically at <when>\". Drop the connective\n // so the caller can compose \"until <when>\" without doubling it up.\n const when = tail.replace(/^at\\s+/i, '').trim();\n if (!when) return null;\n return when.slice(0, 40);\n }\n return null;\n}\n\n/**\n * The usage-limit dialog is on screen but its safe option could not be\n * located, so there is no keystroke we are willing to send (ENG-8213).\n *\n * Callers MUST treat this as \"do not key this pane at all\" and log it: the\n * generic fallbacks (the watchdog's bounded Enter, acceptDialogs' `❯`\n * readiness branch) would otherwise press Enter on whatever row the cursor\n * happens to be sitting on, and on this dialog most of the rows either spend\n * money or page a human.\n */\nexport function isUnanswerableUsageLimitDialog(screen: string): boolean {\n return (\n isUsageLimitChoiceDialogVisible(screen) &&\n findUsageLimitSafeOptionKey(screen) === null\n );\n}\n\n/**\n * Single-pass dialog recognition. Returns the dismissal action for the\n * first recognised dialog on screen, or `null` when no known dialog is\n * visible (including for the login picker, which must never be keyed\n * past — see isLoginPickerVisible).\n *\n * Branch order mirrors the original acceptDialogs() cascade: the theme\n * picker check must run before any generic `❯`-based readiness logic in\n * callers, since picker rows also render with `❯`.\n */\nexport function sweepDialogs(screen: string): DialogAction | null {\n // ENG-8213: FIRST, and it must stay first. The usage-limit dialog renders\n // \"Enter to confirm\", and the mcp-servers branch below fires on\n // `'Enter to confirm' && 'MCP'` — with \"MCP\" trivially present in the\n // scrollback behind the modal, that branch would match this dialog and\n // send a BARE ENTER, confirming whichever row the cursor is on. Two of\n // those three rows are billing actions. Ordering is the guard.\n if (isUsageLimitChoiceDialogVisible(screen)) {\n const safe = findUsageLimitSafeOption(screen);\n // No recognised safe option => answer nothing. isUnanswerableUsageLimitDialog()\n // lets callers detect this state and stop their own fallbacks keying the pane.\n if (!safe) return null;\n return {\n kind: 'usage-limit-choice',\n // The log names the option ACTUALLY picked, not a fixed literal. With two\n // selectable rows now, a constant string would make a fleet sweep unable\n // to tell an agent that will resume by itself from one parked until a\n // human touches it.\n keys: [safe.key, 'Enter'],\n interKeyDelayMs: 300,\n logMessage: `Auto-answered usage-limit choice dialog (picked '${safe.label}')`,\n };\n }\n // ENG-10619: before the generic branches. The dialog renders\n // `Enter to confirm`, and its default row opts the account INTO model\n // training — so nothing below may get the chance to send a bare Enter.\n // Resolve before claiming (see the folder-trust branch): an unresolvable\n // OFF row falls through rather than suppressing later handlers, and\n // isUnclaimedConfirmDialog() still reports the pane.\n const termsKeys = consumerTermsOffKeys(screen);\n if (termsKeys) {\n return {\n kind: 'consumer-terms',\n keys: termsKeys,\n interKeyDelayMs: 300,\n logMessage: \"Auto-answered consumer-terms dialog (picked 'Help improve our AI models: OFF')\",\n };\n }\n if (\n screen.includes('Choose the text style') ||\n (screen.includes('Dark mode') && screen.includes('Light mode'))\n ) {\n return {\n kind: 'theme-picker',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted theme picker',\n };\n }\n // ENG-9575: was a bare Enter. On 2.1.250 this dialog's default row is\n // \"No, exit\", so the bare Enter REFUSED the trust prompt and exited Claude.\n // Latent rather than firing on the hosts that broke (their agent projects\n // were already trusted), but it fires the first time an agent runs in a\n // project directory Claude has not seen — i.e. on every newly provisioned\n // agent. Select the row by name instead.\n //\n // RESOLVE BEFORE CLAIMING. These predicates are whole-pane substring tests,\n // so scrollback or the agent's own prose can match them while a DIFFERENT\n // dialog is focused. Returning null on an unresolved row would cancel the\n // sweep and suppress every branch below — so trust-folder prose sitting\n // behind a real bypass dialog would leave that dialog unanswered, which is\n // the exact wedge this change exists to prevent. Falling through instead\n // keeps the later handlers reachable and lets isUnclaimedConfirmDialog()\n // still report a pane nobody claimed. Same hazard the usage-limit branch\n // documents as the self-gating trap.\n const trustKeys = screen.includes('Yes, I trust this folder')\n ? selectRowKeys(screen, 'Yes, I trust this folder')\n : null;\n if (trustKeys) {\n return {\n kind: 'folder-trust',\n keys: trustKeys,\n interKeyDelayMs: 300,\n logMessage: 'Auto-accepted folder trust',\n };\n }\n // ENG-5364: picks \"Don't ask me again\", which Claude Code persists in config\n // so subsequent resumes skip the dialog entirely.\n // ENG-9575: was ['3','Enter']. The digit is swallowed on 2.1.250's\n // arrow-select shape, leaving Enter to confirm the default row — here that is\n // \"Resume from summary\", which is harmless but is NOT the option that stops\n // the dialog recurring, so the agent re-answered it on every single respawn.\n // Resolve before claiming — see the folder-trust branch above.\n const resumeKeys = isResumeModeDialogVisible(screen)\n ? selectRowKeys(screen, \"Don't ask me again\")\n : null;\n if (resumeKeys) {\n return {\n kind: 'resume-mode',\n keys: resumeKeys,\n interKeyDelayMs: 300,\n logMessage: \"Auto-dismissed resume-mode dialog (picked 'Don't ask me again')\",\n };\n }\n if (screen.includes('I am using this for local development')) {\n return {\n kind: 'dev-channels',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted dev channels',\n };\n }\n // ENG-9224: both shapes of the same dialog — the pre-2.1.237 numbered list\n // and the 2.1.237 checkbox multi-select. Kept in ONE branch, in the position\n // the numbered one already held, so the ordering hazard ENG-8213 fixed (the\n // usage-limit branch must be evaluated first) is untouched by this change.\n if (isMcpServersDialogVisible(screen) || isMcpCheckboxDialogVisible(screen)) {\n return {\n kind: 'mcp-servers',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted MCP servers',\n };\n }\n // ENG-9575: was ['2','Enter'] — the incident branch. See selectRowKeys.\n // Resolve before claiming — see the folder-trust branch above. This one\n // matters most: its two substrings are the ones an agent is likeliest to\n // have written itself, and suppressing the session-feedback branch below\n // leaves a rating prompt swallowing the input box (the koda wedge, ENG-6017).\n const bypassKeys =\n screen.includes('Yes, I accept') && screen.includes('Bypass Permissions')\n ? selectRowKeys(screen, 'Yes, I accept')\n : null;\n if (bypassKeys) {\n return {\n kind: 'bypass-permissions',\n keys: bypassKeys,\n interKeyDelayMs: 300,\n logMessage: 'Auto-accepted bypass permissions',\n };\n }\n // ENG-6017: the rating prompt acts on the bare digit — no Enter needed.\n if (isSessionFeedbackDialogVisible(screen)) {\n return {\n kind: 'session-feedback',\n keys: ['0'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-dismissed session-feedback dialog',\n };\n }\n return null;\n}\n\n/**\n * Send a DialogAction's keystrokes to a tmux session, one send-keys call\n * per key with the action's inter-key delay. execFileSync (not execSync)\n * so the session name is an argv entry rather than shell-interpolated.\n */\nexport async function sendDialogKeys(\n tmuxSession: string,\n action: DialogAction,\n): Promise<void> {\n for (let i = 0; i < action.keys.length; i++) {\n if (i > 0 && action.interKeyDelayMs > 0) {\n await new Promise((r) => setTimeout(r, action.interKeyDelayMs));\n }\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, action.keys[i]!], {\n stdio: 'ignore',\n });\n }\n}\n\n/**\n * ENG-9006 criterion 3: the block of a focused confirm-footer dialog that NO\n * handler in this module claimed, or null.\n *\n * This is the backstop for the failure mode that has now recurred three times\n * in three weeks, each time the same way: Claude Code reworded a modal, every\n * predicate above went false, the pane wedged, and the fleet found out because\n * a human looked at a screenshot.\n *\n * 2026-07-28 ENG-8213 \"Switch to usage credits\" / \"Switch to Team plan\"\n * 2026-08-17 ENG-9006 \"Upgrade your plan\" (two-option variant)\n * 2026-08-19 this \"Wait here, then continue automatically at …\"\n * / \"Ask your admin for more usage\" (Team seat spend)\n *\n * Adding each new string is necessary and is not sufficient — the detector is\n * anchored on vendor-authored copy that the vendor keeps changing, so the NEXT\n * rewording is already scheduled. What does not depend on the wording is the\n * shape: a modal is focused (it renders its confirm affordance) and nothing\n * claimed it.\n *\n * DEFAULT-DENY IS PRESERVED. This function keys nothing and returns no action;\n * it exists purely so an unclaimed modal becomes VISIBLE instead of silent.\n * Answering an unrecognised dialog is still forbidden — a future dialog's\n * default row could be destructive, which is the whole reason sweepDialogs()\n * returns null rather than guessing.\n *\n * Deliberately excludes `isUnanswerableUsageLimitDialog`: that state is\n * recognised, is refused on purpose, and already emits its own loud signal.\n * Counting it here too would double-report one pane.\n */\nexport function focusedConfirmDialogBlock(screen: string): string[] | null {\n const lines = screen.split('\\n');\n // The LIVE dialog is the last footer, same rule as usageLimitModalBlock():\n // an older modal still in the scrollback is not what is focused now.\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i]!.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n }\n if (footer < 0) return null;\n return lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer + 1);\n}\n\n/**\n * True when a focused confirm-footer dialog is on screen and no handler in\n * this module claimed it. See focusedConfirmDialogBlock for why this exists.\n */\nexport function isUnclaimedConfirmDialog(screen: string): boolean {\n if (focusedConfirmDialogBlock(screen) === null) return false;\n if (sweepDialogs(screen) !== null) return false;\n // Already has its own signal — see the doc above.\n if (isUnanswerableUsageLimitDialog(screen)) return false;\n return true;\n}\n\n/**\n * Tiny non-cryptographic hash for hash-only logging of channel input\n * (prod logging policy: input may contain PII/secrets, so log hash+len,\n * never content). Shared by the watchdog and the inject-time hygiene.\n */\nexport function simpleTextHash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) {\n h = ((h << 5) - h + s.charCodeAt(i)) | 0;\n }\n return h.toString(16);\n}\n\n/**\n * ENG-9932: a pane state that NO keystroke this module is willing to send can\n * clear, and that therefore a session RESTART cannot clear either.\n *\n * The distinction this type draws is the whole point of the ticket. On\n * agt-aws-1 (2026-09-03) eight agents were auto-paused because\n * `mcp-presence-reaper` saw \"declared MCP(s) have no live children\" and\n * restarted the session — three times in ten minutes, into the circuit breaker\n * (ENG-5441). The MCP servers were not broken. Claude Code had never finished\n * booting, because it was sitting on the login picker, so no MCP child had ever\n * been spawned. Every restart re-presented the same modal.\n *\n * `no live MCP children` is therefore a SYMPTOM with (at least) two causes that\n * want opposite remedies:\n *\n * - an MCP server actually died -> restart the session (correct)\n * - the pane is blocked on a modal -> restarting is futile by\n * construction; surface it\n *\n * Nothing distinguished them, so the reaper applied the first remedy to the\n * second cause and the diagnosis landed two subsystems away from the fault.\n */\n/**\n * Every pane state a restart provably cannot recover.\n *\n * A RUNTIME array, with the type DERIVED from it, deliberately. The obvious\n * shape is a bare union plus a hand-written list in the test — but a\n * three-element `UnanswerablePaneKind[]` stays a valid `UnanswerablePaneKind[]`\n * after the union grows to four, so the coverage assertion would keep passing\n * while the new kind shipped untested. A `Record<UnanswerablePaneKind, true>` in\n * the test does not fix it either: `apps/cli/tsconfig.json` excludes\n * `**\\/*.test.ts`, and vitest strips types without checking them, so a\n * compile-time guard written in a test file is never evaluated by anything.\n *\n * Exporting the values and deriving the type means adding a kind here makes the\n * suite's coverage check fail at RUNTIME, which is the only mechanism that\n * actually runs.\n */\nexport const UNANSWERABLE_PANE_KINDS = [\n /** The login picker (ENG-4634). Needs an operator OAuth; no keystroke and no\n * respawn can complete it, and `acceptDialogs()` deliberately never keys\n * past it. This is the state marlow was found in. */\n 'login-picker',\n /** The usage-limit modal is up but no option we are willing to press is on\n * it (ENG-8213). Recognised and refused ON PURPOSE — every remaining row\n * either spends money or pages a human. A respawn re-presents it. */\n 'usage-limit-unanswerable',\n /** A modal is focused and NO handler in this module claimed it (ENG-9006).\n * Unrecognised means neither the channel-input watchdog nor the spawn-time\n * `acceptDialogs()` cascade can answer it, so a respawn lands on the same\n * prompt. This is the branch that catches the NEXT dialog Claude Code adds,\n * which is the one we cannot enumerate in advance. */\n 'unclaimed-modal',\n] as const;\n\nexport type UnanswerablePaneKind = (typeof UNANSWERABLE_PANE_KINDS)[number];\n\n/**\n * ENG-10972 hardening: the kind of the modal that OWNS THE BOTTOM of the pane -\n * the one the session is actually sitting on - or null when none does.\n *\n * `classifyUnanswerablePane` is a whole-screen substring test, and measured on\n * 2026-09-24 it classifies all four of these as blocked: an agent's message that\n * mentions \"Select login method\", a code line containing the picker's option\n * text, and a usage-limit or unknown modal QUOTED mid-transcript with a live input\n * box below it (`focusedConfirmDialogBlock` finds the LAST \"Enter to confirm\"\n * anywhere on screen). The reaper and spawn-dialogs can use it anyway because they\n * carry a corroborating prior (see `BlockedPromptSource` in\n * session-blocked-prompt.ts). A per-poll watchdog sweep has none, so it uses this.\n *\n * Grounded in the real panes, not guessed: a live confirm modal ends on its\n * footer, and the host-captured login picker (eng-9932's LOGIN_PICKER_PANE) ends\n * on an option row. Anything after that - an input box, more transcript - means\n * the session is not parked on it.\n *\n * The kind is read off WHAT owns the last line, never off the whole screen\n * (CodeRabbit, #6485). The first version checked \"some modal owns the bottom\" and\n * then classified the full pane, where the login-picker test runs first - so a\n * usage-limit modal parked at the bottom with \"Select login method\" anywhere above\n * it was reported as a login picker. So:\n * - a confirm footer at the bottom is classified from its own focused block, and\n * the login-picker substring test never runs (the picker has no such footer);\n * - a picker option row at the bottom is a login picker, and nothing else.\n * Even the 7-line focused block can carry a transcript line above a short modal,\n * which is why the confirm branch checks only the usage-limit and unclaimed kinds.\n * The one question it does NOT scope to the block is \"do we answer this dialog\n * ourselves?\" (sweepDialogs): that text can sit above the block (see below).\n *\n * Deliberately NOT folded into `classifyUnanswerablePane`: tightening that would\n * change the reaper's and spawn's behaviour without live-pane evidence that no\n * Claude Code version renders a hint line under its modals. For the watchdog's\n * busy-ack marker a false negative only restores the old generic notice, so this\n * is the safe place to be strict.\n */\nexport function parkedModalKind(screen: string | null | undefined): UnanswerablePaneKind | null {\n if (!screen) return null;\n const lines = screen.split('\\n').filter((l) => l.trim() !== '');\n const last = lines[lines.length - 1] ?? '';\n if (last.includes('Enter to confirm')) {\n const block = focusedConfirmDialogBlock(screen);\n if (!block) return null;\n const text = block.join('\\n');\n if (isUnanswerableUsageLimitDialog(text)) return 'usage-limit-unanswerable';\n // Obi Wan 9307eb1cbdd8: \"is this a dialog we ANSWER?\" is a whole-screen\n // question. sweepDialogs recognises dialogs by text that can sit far above the\n // footer - the real Bypass Permissions pane's is 11+ lines up, outside the\n // 7-line block - so a block-only check turned claimed dialogs into \"unclaimed\"\n // and marked an agent stuck on a prompt acceptDialogs was about to answer.\n // A claimed dialog merely QUOTED above an unknown modal now reads as claimed:\n // a false negative, which only restores the generic busy notice.\n if (sweepDialogs(screen) !== null) return null;\n if (isUnclaimedConfirmDialog(text)) return 'unclaimed-modal';\n return null;\n }\n if (/Claude account with subscription|Anthropic Console account|3rd-party platform/.test(last)) {\n return isLoginPickerVisible(screen) ? 'login-picker' : null;\n }\n return null;\n}\n\n/**\n * Classify a pane that a session restart provably cannot recover, or null.\n *\n * ## Why a RECOGNISED, answerable dialog is deliberately not in this set\n *\n * The tempting version of this function returns non-null for any dialog at\n * all. That would be wrong, and in the dangerous direction: it would add a new\n * way to suppress a restart that is genuinely needed.\n *\n * A dialog `sweepDialogs()` claims gets answered by two independent paths — the\n * per-poll `channel-input-watchdog`, and the spawn-time `acceptDialogs()`\n * cascade in persistent-session.ts. The second one means a restart is a real\n * remedy for an answerable dialog: the fresh session hits the same prompt and\n * the cascade keys past it. So an answerable dialog must keep the pre-ENG-9932\n * behaviour and fall through to the restart.\n *\n * Every kind above is the opposite case: no path answers it, in this session or\n * the next. Holding the restart costs nothing that a restart would have gained,\n * and saves the breaker budget that today gets burned discovering that.\n *\n * DEFAULT-DENY IS PRESERVED, and note the direction. This function keys\n * nothing and returns no `DialogAction`; a pane it does not recognise returns\n * null, which means \"carry on with the restart\" — i.e. unrecognised falls\n * through to the EXISTING behaviour, never to a new suppression.\n */\nexport function classifyUnanswerablePane(\n screen: string | null | undefined,\n): UnanswerablePaneKind | null {\n // A pane we could not read is not evidence of a blocked pane. Fail open to\n // the legacy restart path: a tmux hiccup must never become a silent,\n // fleet-wide suppression of MCP recovery.\n if (!screen) return null;\n if (isLoginPickerVisible(screen)) return 'login-picker';\n if (isUnanswerableUsageLimitDialog(screen)) return 'usage-limit-unanswerable';\n if (isUnclaimedConfirmDialog(screen)) return 'unclaimed-modal';\n return null;\n}\n\n/**\n * A CONTENT-FREE fingerprint of the blocking modal: a stable hash plus the\n * block's length — or null when there is no focused modal block.\n *\n * ## Why a hash and not the text (CodeRabbit, PR #5504)\n *\n * The first version of this returned a 200-character excerpt of the pane. The\n * bound was the wrong control. The modal this fires on most often is the\n * UNCLAIMED one, whose content is by definition unknown to us, and the block is\n * simply the six lines above a confirm footer — which on a busy agent is the\n * transcript it was mid-way through writing. Truncating a customer's message to\n * 200 characters still forwards a customer's message, and `onPaneBlocked`\n * persists this into a control-plane alert row, so it would leave the host.\n *\n * This module's own `simpleTextHash` already states the policy this has to\n * follow — \"input may contain PII/secrets, so log hash+len, never content\" —\n * and `channel-input-watchdog` already reports exactly this data, the unclaimed\n * dialog block, as `unclaimedDialogHash`. The decision was made; this just\n * failed to follow it.\n *\n * Nothing diagnostic is lost. The `UnanswerablePaneKind` is the actual\n * diagnosis, and the operator-facing line names the one action that resolves\n * it (`tmux attach`), where the real pane is a keystroke away. The hash adds\n * the thing an excerpt could not: correlation. Eight agents blocked on the same\n * unknown modal share a hash, which is how you tell one wedged agent from a\n * fleet-wide dialog rollout.\n */\nexport function summarizeUnanswerablePane(\n screen: string,\n): { hash: string; length: number } | null {\n const block = focusedConfirmDialogBlock(screen);\n if (!block) return null;\n const flat = block.join('\\n');\n return { hash: simpleTextHash(flat), length: flat.length };\n}\n"],"mappings":";AA8BA,SAAS,oBAAoB;AAoDtB,SAAS,qBAAqB,QAAyB;AAC5D,SACE,OAAO,SAAS,qBAAqB,KACpC,OAAO,SAAS,kCAAkC,KACjD,OAAO,SAAS,2BAA2B;AAEjD;AAmBO,IAAM,4BACX;AAQK,SAAS,6BAA6B,QAAyB;AACpE,SACE,OAAO,SAAS,2BAA2B,KAC3C,OAAO,SAAS,yBAAyB;AAE7C;AAQO,SAAS,qBAAqB,QAA0C;AAC7E,MAAI,CAAC,6BAA6B,MAAM,EAAG,QAAO;AAClD,SAAO,cAAc,QAAQ,yBAAyB;AACxD;AA2BO,SAAS,WAAW,UAAkB,sBAA8C;AACzF,QAAM,SAAS,sBAAsB,QAAQ;AAC7C,MAAI,yBAAyB,MAAM;AACjC;AAAA;AAAA;AAAA;AAAA,MAIE,GAAG,MAAM,oIAC8C,yBAAyB;AAAA;AAAA,EAEpF;AACA,MAAI,yBAAyB,MAAM;AACjC,WACE,GAAG,MAAM,0FACE,yBAAyB;AAAA,EAExC;AACA,SAAO;AACT;AAqBO,SAAS,0BAA0B,QAAyB;AACjE,SACE,OAAO,SAAS,qBAAqB,KACrC,OAAO,SAAS,oBAAoB;AAExC;AAoBO,SAAS,+BAA+B,QAAyB;AACtE,SACE,OAAO,SAAS,kCAAkC,KAClD,OAAO,SAAS,YAAY;AAEhC;AAWO,IAAM,0BAA0B;AAWhC,IAAM,8BAA8B;AAsBpC,IAAM,2BAA8C;AAAA,EACzD;AAAA,EACA;AACF;AA4BA,IAAM,4BAA+C;AAAA,EACnD,GAAG;AAAA,EACH;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,EACA;AACF;AAmDA,IAAM,0BAA0B;AAEhC,SAAS,eAAe,MAAc,OAA8B;AAClE,QAAM,IAAI,KAAK;AAAA,IACb,IAAI;AAAA,MACF,OAAO,6CACL,MAAM,QAAQ,uBAAuB,OAAO,QAAQ;AAAA,IACxD;AAAA,EACF;AACA,SAAO,IAAI,CAAC,KAAK;AACnB;AAiCA,SAAS,kBAAkB,QAAgB,YAAqC;AAC9E,QAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,MAAI,SAAS;AACb,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;AAC1C,QAAI,MAAM,CAAC,EAAG,SAAS,kBAAkB,GAAG;AAC1C,eAAS;AACT;AAAA,IACF;AAAA,EACF;AACA,MAAI,SAAS,EAAG,QAAO;AACvB,SAAO,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS,UAAU,GAAG,MAAM;AAC7D;AAWA,IAAM,qBAAwC;AAAA,EAC5C;AAAA,EACA;AACF;AAwBO,SAAS,0BAA0B,QAAyB;AACjE,QAAM,QAAQ,kBAAkB,QAAQ,uBAAuB;AAC/D,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,KAAK,CAAC,MAAM,mBAAmB,KAAK,CAAC,UAAU,eAAe,GAAG,KAAK,MAAM,IAAI,CAAC;AAChG;AA0CA,IAAM,sBAAsB;AAQ5B,IAAM,kBAAkB;AAUxB,IAAM,2BAA2B;AAE1B,SAAS,2BAA2B,QAAyB;AAClE,QAAM,QAAQ,kBAAkB,QAAQ,wBAAwB;AAChE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,CAAC,MAAM,KAAK,CAAC,MAAM,oBAAoB,KAAK,CAAC,CAAC,EAAG,QAAO;AAC5D,SAAO,MAAM,KAAK,CAAC,MAAM,gBAAgB,KAAK,CAAC,CAAC;AAClD;AAEA,SAAS,qBAAqB,QAAiC;AAC7D,QAAM,QAAQ,OAAO,MAAM,IAAI;AAS/B,MAAI,SAAS;AACb,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;AAC1C,QAAI,MAAM,CAAC,EAAG,SAAS,kBAAkB,GAAG;AAC1C,eAAS;AACT;AAAA,IACF;AAAA,EACF;AACA,MAAI,SAAS,EAAG,QAAO;AACvB,QAAM,QAAQ,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS,uBAAuB,GAAG,MAAM;AAC/E,QAAM,oBAAoB,MAAM;AAAA,IAAK,CAAC,MACpC,0BAA0B,KAAK,CAAC,UAAU,eAAe,GAAG,KAAK,MAAM,IAAI;AAAA,EAC7E;AACA,SAAO,oBAAoB,QAAQ;AACrC;AA4BO,SAAS,gCAAgC,QAAyB;AACvE,SAAO,qBAAqB,MAAM,MAAM;AAC1C;AAmBO,SAAS,yBACd,QACuC;AAGvC,QAAM,QAAQ,qBAAqB,MAAM;AACzC,MAAI,CAAC,MAAO,QAAO;AAOnB,aAAW,SAAS,0BAA0B;AAC5C,eAAW,QAAQ,OAAO;AACxB,YAAM,QAAQ,eAAe,MAAM,KAAK;AACxC,UAAI,UAAU,KAAM,QAAO,EAAE,KAAK,OAAO,MAAM;AAAA,IACjD;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,4BAA4B,QAA+B;AACzE,SAAO,yBAAyB,MAAM,GAAG,OAAO;AAClD;AAOA,IAAM,sBAAsB;AAG5B,SAAS,eAAkB,IAAkB,MAAiC;AAC5E,WAAS,IAAI,GAAG,SAAS,GAAG,KAAK,GAAG,KAAK;AACvC,QAAI,KAAK,GAAG,CAAC,CAAE,EAAG,QAAO;AAAA,EAC3B;AACA,SAAO;AACT;AA6BA,SAAS,YAAY,MAAuB;AAC1C,SAAO,uBAAuB,KAAK,IAAI;AACzC;AAEA,SAAS,eAAe,MAAc,OAAwB;AAC5D,QAAM,UAAU,MAAM,QAAQ,uBAAuB,OAAO,QAAQ;AACpE,SAAO,IAAI;AAAA,IACT,OAAO,gDACL,UACA,OAAO;AAAA,EACX,EAAE,KAAK,IAAI;AACb;AAkCO,SAAS,cACd,QACA,OAC0B;AAQ1B,QAAM,QAAQ,kBAAkB,QAAQ,mBAAmB;AAC3D,MAAI,CAAC,MAAO,QAAO;AAInB,QAAM,SAAS,eAAe,OAAO,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AACpE,MAAI,SAAS,EAAG,QAAO;AAKvB,QAAM,QAAQ,eAAe,MAAM,MAAM,GAAI,KAAK;AAClD,MAAI,UAAU,KAAM,QAAO,CAAC,OAAO,OAAO;AAK1C,QAAM,UAAoB,CAAC;AAC3B,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,YAAY,MAAM,CAAC,CAAE,EAAG,SAAQ,KAAK,CAAC;AAAA,EAC5C;AAIA,MAAI,QAAQ,WAAW,EAAG,QAAO;AAMjC,MAAI,SAAS,QAAQ,CAAC;AACtB,MAAI,OAAO;AACX,aAAW,KAAK,QAAQ,MAAM,CAAC,GAAG;AAChC,UAAM,IAAI,KAAK,IAAI,IAAI,MAAM;AAC7B,UAAM,OAAO,KAAK,IAAI,SAAS,MAAM;AACrC,QAAI,IAAI,MAAM;AACZ,eAAS;AACT,aAAO;AAAA,IACT,WAAW,MAAM,MAAM;AACrB,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,KAAM,QAAO;AAEjB,QAAM,WAAW,SAAS;AAC1B,MAAI,aAAa,EAAG,QAAO,CAAC,OAAO;AACnC,QAAM,OAAO,WAAW,IAAI,SAAS;AACrC,SAAO,CAAC,GAAG,MAAc,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,IAAI,GAAG,OAAO;AAClE;AAyBO,SAAS,wBAAwB,QAA+B;AACrE,QAAM,QAAQ,qBAAqB,MAAM;AACzC,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,QAAQ,OAAO;AACxB,QAAI,eAAe,MAAM,2BAA2B,MAAM,KAAM;AAChE,UAAM,MAAM,KAAK,QAAQ,2BAA2B;AACpD,UAAM,OAAO,KAAK,MAAM,MAAM,4BAA4B,MAAM,EAAE,KAAK;AAGvE,UAAM,OAAO,KAAK,QAAQ,WAAW,EAAE,EAAE,KAAK;AAC9C,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,KAAK,MAAM,GAAG,EAAE;AAAA,EACzB;AACA,SAAO;AACT;AAYO,SAAS,+BAA+B,QAAyB;AACtE,SACE,gCAAgC,MAAM,KACtC,4BAA4B,MAAM,MAAM;AAE5C;AAYO,SAAS,aAAa,QAAqC;AAOhE,MAAI,gCAAgC,MAAM,GAAG;AAC3C,UAAM,OAAO,yBAAyB,MAAM;AAG5C,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO;AAAA,MACL,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,MAKN,MAAM,CAAC,KAAK,KAAK,OAAO;AAAA,MACxB,iBAAiB;AAAA,MACjB,YAAY,oDAAoD,KAAK,KAAK;AAAA,IAC5E;AAAA,EACF;AAOA,QAAM,YAAY,qBAAqB,MAAM;AAC7C,MAAI,WAAW;AACb,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,MACN,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AAAA,EACF;AACA,MACE,OAAO,SAAS,uBAAuB,KACtC,OAAO,SAAS,WAAW,KAAK,OAAO,SAAS,YAAY,GAC7D;AACA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,CAAC,OAAO;AAAA,MACd,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AAAA,EACF;AAiBA,QAAM,YAAY,OAAO,SAAS,0BAA0B,IACxD,cAAc,QAAQ,0BAA0B,IAChD;AACJ,MAAI,WAAW;AACb,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,MACN,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AAAA,EACF;AAQA,QAAM,aAAa,0BAA0B,MAAM,IAC/C,cAAc,QAAQ,oBAAoB,IAC1C;AACJ,MAAI,YAAY;AACd,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,MACN,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AAAA,EACF;AACA,MAAI,OAAO,SAAS,uCAAuC,GAAG;AAC5D,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,CAAC,OAAO;AAAA,MACd,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AAAA,EACF;AAKA,MAAI,0BAA0B,MAAM,KAAK,2BAA2B,MAAM,GAAG;AAC3E,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,CAAC,OAAO;AAAA,MACd,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AAAA,EACF;AAMA,QAAM,aACJ,OAAO,SAAS,eAAe,KAAK,OAAO,SAAS,oBAAoB,IACpE,cAAc,QAAQ,eAAe,IACrC;AACN,MAAI,YAAY;AACd,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,MACN,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AAAA,EACF;AAEA,MAAI,+BAA+B,MAAM,GAAG;AAC1C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,CAAC,GAAG;AAAA,MACV,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;AAOA,eAAsB,eACpB,aACA,QACe;AACf,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK,QAAQ,KAAK;AAC3C,QAAI,IAAI,KAAK,OAAO,kBAAkB,GAAG;AACvC,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,OAAO,eAAe,CAAC;AAAA,IAChE;AACA,iBAAa,QAAQ,CAAC,aAAa,MAAM,aAAa,OAAO,KAAK,CAAC,CAAE,GAAG;AAAA,MACtE,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAgCO,SAAS,0BAA0B,QAAiC;AACzE,QAAM,QAAQ,OAAO,MAAM,IAAI;AAG/B,MAAI,SAAS;AACb,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;AAC1C,QAAI,MAAM,CAAC,EAAG,SAAS,kBAAkB,GAAG;AAC1C,eAAS;AACT;AAAA,IACF;AAAA,EACF;AACA,MAAI,SAAS,EAAG,QAAO;AACvB,SAAO,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS,uBAAuB,GAAG,SAAS,CAAC;AAC9E;AAMO,SAAS,yBAAyB,QAAyB;AAChE,MAAI,0BAA0B,MAAM,MAAM,KAAM,QAAO;AACvD,MAAI,aAAa,MAAM,MAAM,KAAM,QAAO;AAE1C,MAAI,+BAA+B,MAAM,EAAG,QAAO;AACnD,SAAO;AACT;AAOO,SAAS,eAAe,GAAmB;AAChD,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,SAAM,KAAK,KAAK,IAAI,EAAE,WAAW,CAAC,IAAK;AAAA,EACzC;AACA,SAAO,EAAE,SAAS,EAAE;AACtB;AAgGO,SAAS,gBAAgB,QAAgE;AAC9F,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,OAAO,MAAM,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,MAAM,EAAE;AAC9D,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC,KAAK;AACxC,MAAI,KAAK,SAAS,kBAAkB,GAAG;AACrC,UAAM,QAAQ,0BAA0B,MAAM;AAC9C,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,OAAO,MAAM,KAAK,IAAI;AAC5B,QAAI,+BAA+B,IAAI,EAAG,QAAO;AAQjD,QAAI,aAAa,MAAM,MAAM,KAAM,QAAO;AAC1C,QAAI,yBAAyB,IAAI,EAAG,QAAO;AAC3C,WAAO;AAAA,EACT;AACA,MAAI,gFAAgF,KAAK,IAAI,GAAG;AAC9F,WAAO,qBAAqB,MAAM,IAAI,iBAAiB;AAAA,EACzD;AACA,SAAO;AACT;AA2BO,SAAS,yBACd,QAC6B;AAI7B,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,qBAAqB,MAAM,EAAG,QAAO;AACzC,MAAI,+BAA+B,MAAM,EAAG,QAAO;AACnD,MAAI,yBAAyB,MAAM,EAAG,QAAO;AAC7C,SAAO;AACT;AA6BO,SAAS,0BACd,QACyC;AACzC,QAAM,QAAQ,0BAA0B,MAAM;AAC9C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,MAAM,KAAK,IAAI;AAC5B,SAAO,EAAE,MAAM,eAAe,IAAI,GAAG,QAAQ,KAAK,OAAO;AAC3D;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/claude-dialogs.ts"],"sourcesContent":["/**\n * ENG-6017: shared Claude Code TUI dialog detection + dismissal.\n *\n * Extracted from persistent-session.ts's acceptDialogs() cascade so the\n * dialog knowledge has exactly one home, consumable from:\n *\n * - `acceptDialogs()` in persistent-session.ts — the spawn-time loop that\n * walks an agent through the first-run dialog cascade (theme → trust →\n * MCP → bypass …).\n * - `channel-input-watchdog.ts` — the per-poll-cycle watchdog that fires\n * Enter at stuck channel input. Before ENG-6017 the watchdog was\n * dialog-blind: when Claude Code's session-feedback dialog (\"How is\n * Claude doing this session?\") overlaid the pane, the watchdog's\n * single-shot Enter went into the dialog instead of the input box and\n * the inbound message sat unsubmitted for 40+ minutes (koda,\n * 2026-06-04) while every health metric stayed green.\n * - `injectMessageWithStatus()` — pre-send pane hygiene on the tmux\n * send-keys fallback path.\n *\n * Living in its own module (rather than persistent-session.ts) breaks the\n * import cycle persistent-session → channel-input-watchdog →\n * persistent-session that a shared export would otherwise create.\n *\n * DEFAULT-DENY: `sweepDialogs()` only ever returns an action for an\n * explicitly recognised dialog. An unknown dialog gets `null` — never a\n * blind Enter — because a future dialog's default option could be\n * destructive. Callers that want visibility into unknown overlays should\n * log the pane themselves.\n */\n\nimport { execFileSync } from 'node:child_process';\n\n/**\n * A recognised dialog plus the keystrokes that dismiss it.\n *\n * `keys` are tmux key names sent as individual `tmux send-keys` calls —\n * never batched into one call, so a multi-byte sequence can't get wrapped\n * into a single bracketed paste (the CSI-u trap documented on\n * defaultArmSender in persistent-session.ts).\n */\nexport interface DialogAction {\n kind:\n | 'theme-picker'\n | 'folder-trust'\n | 'resume-mode'\n | 'dev-channels'\n | 'mcp-servers'\n | 'bypass-permissions'\n | 'session-feedback'\n | 'usage-limit-choice'\n | 'consumer-terms';\n /** tmux key names, one send-keys invocation each. */\n keys: readonly string[];\n /** Delay between consecutive key sends (selector dialogs need a beat\n * between picking an option and confirming it). */\n interKeyDelayMs: number;\n /** Past-tense log fragment, e.g. \"Auto-accepted theme picker\". Callers\n * append their own context (`for '<codeName>'` etc.). */\n logMessage: string;\n}\n\n/**\n * Detect whether Claude Code is showing the **login picker** dialog.\n *\n * ENG-4634: this dialog appears when ~/.claude.json is missing or\n * Claude Code can't validate the saved session. Pressing Enter on\n * the default (1. Claude account with subscription) kicks off a\n * browser-based OAuth flow that an unattended agent can't complete —\n * the helper used to fall through to the generic `❯ no Enter to\n * confirm` exit branch and declare the session \"ready\" while the\n * actual claude REPL was still on the picker. Without explicit\n * detection, every manager respawn would silently flip the agent\n * back to the picker and never recover.\n *\n * Pattern matches the literal option strings claude renders. Both\n * 'Claude account with subscription' and 'Anthropic Console account'\n * are present on the picker (and not in the post-login UI), so the\n * conjunction is unambiguous.\n *\n * NOT part of sweepDialogs() — the picker must never be keyed past\n * (it needs an operator OAuth); acceptDialogs() handles it specially.\n */\nexport function isLoginPickerVisible(screen: string): boolean {\n return (\n screen.includes('Select login method') ||\n (screen.includes('Claude account with subscription') &&\n screen.includes('Anthropic Console account'))\n );\n}\n\n/**\n * ENG-10619: the row we ALWAYS pick on Claude Code's consumer-terms dialog.\n *\n * Updates to Consumer Terms and Policies\n * …\n * ❯ 1. Accept terms · Help improve our AI models: ON\n * 2. Accept terms · Help improve our AI models: OFF\n *\n * Enter to confirm · Esc to cancel\n *\n * The cursor defaults to ON, which opts the account's chats and coding\n * sessions into model training and extends retention from 30 days to 5\n * years. A bare Enter would therefore consent on the customer's behalf, so\n * this row is selected by NAME (selectRowKeys) and never by position or\n * default. The `·` is U+00B7, the same separator Claude Code uses in its\n * confirm footer.\n */\nexport const CONSUMER_TERMS_OFF_OPTION =\n 'Accept terms \\u00b7 Help improve our AI models: OFF';\n\n/**\n * ENG-10619: the consumer-terms dialog is on screen. Keyed on the\n * conjunction of the heading and the OFF row's own text so prose that\n * mentions either alone does not match; selectRowKeys still requires a\n * focused modal and a complete option row before anything is keyed.\n */\nexport function isConsumerTermsDialogVisible(screen: string): boolean {\n return (\n screen.includes('Updates to Consumer Terms') &&\n screen.includes(CONSUMER_TERMS_OFF_OPTION)\n );\n}\n\n/**\n * ENG-10619: keys that pick \"Help improve our AI models: OFF\" on a focused\n * consumer-terms dialog, or null (answer nothing) when that row cannot be\n * resolved. Shared by sweepDialogs() and the Claude pair/re-login flow,\n * which drives its own dialog loop.\n */\nexport function consumerTermsOffKeys(screen: string): readonly string[] | null {\n if (!isConsumerTermsDialogVisible(screen)) return null;\n return selectRowKeys(screen, CONSUMER_TERMS_OFF_OPTION);\n}\n\n/**\n * ENG-10972: the operator-facing remedy for a session a human has to unstick at the\n * pane. EVERY \"needs a human\" line goes through this, and that is the point.\n *\n * On 2026-09-24 three Primo Cards agents sat blocked on the rate-limit prompt with\n * the consumer-terms dialog ALSO on the pane. The remedy text said only \"tmux\n * attach\". That dialog's cursor defaults to \"Help improve our AI models: ON\", which\n * opts the CUSTOMER's account into model training and stretches retention from 30\n * days to 5 years. The operator who cleared them by hand was one Enter away from\n * consenting on a customer's behalf, three times, and nothing he was shown said so.\n *\n * The blocking KIND never says \"consumer-terms\": on those panes the blocker was the\n * rate-limit prompt, and the consent dialog sat beside it. So the caller passes\n * whether a consent dialog is VISIBLE on the pane. That is a boolean, which is not\n * pane content, so it may leave the host in an alert row. The pane text itself may\n * not (see summarizeUnanswerablePane).\n *\n * Three states, because one site speaks before the pane has been read:\n * true -> name the safe row, and say not to press Enter\n * false -> the bare attach command; no warning on every hold, or it becomes noise\n * null -> we did not look, so a CONDITIONAL caution rather than silence\n *\n * The named row is `CONSUMER_TERMS_OFF_OPTION`, the same constant the automated\n * sweeper selects by name, so the human and the machine give the same answer.\n */\nexport function paneRemedy(codeName: string, consentDialogVisible: boolean | null): string {\n const attach = `tmux attach -t agt-${codeName}`;\n if (consentDialogVisible === true) {\n return (\n // Never glue punctuation to the command: `${attach}.` is copied as a session\n // named \"agt-<name>.\" that does not exist. Same \" (...)\" shape as the null\n // branch, and single-line because every caller embeds this mid-sentence.\n `${attach} (A CONSENT DIALOG IS ON THIS PANE: do NOT press Enter - the cursor defaults ` +\n `to opting this account into model training. Select \"${CONSUMER_TERMS_OFF_OPTION}\" by name, or Esc)`\n );\n }\n if (consentDialogVisible === null) {\n return (\n `${attach} (if the Consumer Terms dialog is showing, do NOT press Enter on the default - ` +\n `select \"${CONSUMER_TERMS_OFF_OPTION}\" by name)`\n );\n }\n return attach;\n}\n\n/**\n * Detect Claude Code's resume-mode dialog (ENG-5364).\n *\n * On `claude --resume <uuid>` against a transcript large enough to\n * trigger Claude Code 2.1.x's context-management heuristic, the agent\n * lands on an interactive picker offering:\n *\n * ❯ 1. Resume from summary (recommended)\n * 2. Resume full session as-is\n * 3. Don't ask me again\n *\n * Without auto-dismissal the agent sits silently waiting for keyboard\n * input on every manager respawn — channel inbounds stack up while\n * health metrics stay green. Surfaced fleet-wide on 2026-05-20\n * (don/stirling/maven hit it on a single manager restart).\n *\n * Match on the conjunction of two distinct option strings so a\n * passing mention of \"Resume\" in a transcript doesn't false-positive.\n */\nexport function isResumeModeDialogVisible(screen: string): boolean {\n return (\n screen.includes('Resume from summary') &&\n screen.includes(\"Don't ask me again\")\n );\n}\n\n/**\n * Detect Claude Code's session-feedback dialog (ENG-6017).\n *\n * After some turns Claude Code renders an optional rating prompt:\n *\n * ● How is Claude doing this session? (optional)\n * 1: Bad 2: Fine 3: Good 0: Dismiss\n *\n * It waits for a digit — Enter does nothing — so any injected message\n * sits in the input box unsubmitted, and the channel-input-watchdog's\n * Enter is swallowed too. Observed live on koda (agt-aws-1) 2026-06-04:\n * an operator Slack DM sat typed-but-unsubmitted for 40+ minutes behind\n * this dialog while pane-activity / synthetic-probe / heartbeat all\n * stayed green.\n *\n * Match the question text together with the literal `0: Dismiss` option\n * so a transcript merely *quoting* the question doesn't false-positive.\n */\nexport function isSessionFeedbackDialogVisible(screen: string): boolean {\n return (\n screen.includes('How is Claude doing this session') &&\n screen.includes('0: Dismiss')\n );\n}\n\n/**\n * The stop-the-session safe option (ENG-8213). Exported so tests and callers\n * assert against the same literal the matcher uses, rather than a copy that\n * could drift.\n *\n * Kept as the name it has always had because callers and tests reference it,\n * but it is no longer the ONLY option this module selects — see\n * `USAGE_LIMIT_SAFE_OPTIONS` for the full preference order.\n */\nexport const USAGE_LIMIT_SAFE_OPTION = 'Stop and wait for limit to reset';\n\n/**\n * The auto-continue option, added by Claude Code on the Team seat spend-limit\n * variant of this modal:\n *\n * 2. Wait here, then continue automatically at Aug 21, 10am\n *\n * A PREFIX, deliberately: the row carries a reset date that changes every\n * time, so the literal can only ever be the stable leading text.\n */\nexport const USAGE_LIMIT_CONTINUE_OPTION = 'Wait here, then continue automatically';\n\n/**\n * Options this module is willing to press, MOST PREFERRED FIRST.\n *\n * `Wait here, then continue automatically` outranks `Stop and wait for limit to\n * reset` because the two differ in exactly the way that matters to an\n * unattended agent: both idle until the cap lifts, but only the first RESUMES\n * by itself afterwards. \"Stop and wait\" leaves the session parked until a human\n * touches it, which is the state this whole handler exists to avoid — we would\n * have answered the modal and still needed the human.\n *\n * Neither spends money, which is the invariant that governs this list. Every\n * other row on either variant of this modal is excluded on purpose:\n *\n * - `Switch to usage credits` — starts pay-as-you-go spend.\n * - `Switch to Team plan` — a plan change.\n * - `Ask your admin for more usage` — spends nothing, but it pages a human\n * and asks them to raise a budget. That is a decision with a cost attached\n * and it is not ours to make automatically; a human choosing to raise the\n * cap is a different act from a fleet of agents asking them to.\n */\nexport const USAGE_LIMIT_SAFE_OPTIONS: readonly string[] = [\n USAGE_LIMIT_CONTINUE_OPTION,\n USAGE_LIMIT_SAFE_OPTION,\n];\n\n/**\n * Every option label known to appear on this modal, including the ones we would\n * never press. This is the RECOGNITION vocabulary, and it is deliberately wider\n * than the selection vocabulary above.\n *\n * ENG-8213 anchored recognition on the BILLING rows alone, which tied \"is this\n * the usage-limit modal?\" to options that only exist on the subscription\n * variant. Claude Code's Team seat spend-limit variant renders\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Wait here, then continue automatically at Aug 21, 10am\n * 3. Ask your admin for more usage\n *\n * with no billing row at all, so the modal was not recognised — and the failure\n * was not a no-op. This pane renders \"Enter to confirm\", and the `mcp-servers`\n * branch below fires on `'Enter to confirm' && 'MCP'` with \"MCP\" trivially\n * present in any agent's scrollback. So the unrecognised variant fell through to\n * a BARE ENTER, confirming whichever row the cursor sat on — row 1, \"Stop and\n * wait\", the one option that guarantees a human is needed. Observed on\n * acquire-intelligence, 2026-08-19.\n *\n * Recognising by \"an option row this modal is known to render\" rather than by\n * billing vocabulary keeps the structural guard (it must still be a numbered\n * ROW inside the block adjacent to the nearest footer) while covering both\n * variants and any future one whose rows we add here.\n */\nconst USAGE_LIMIT_KNOWN_OPTIONS: readonly string[] = [\n ...USAGE_LIMIT_SAFE_OPTIONS,\n 'Switch to usage credits',\n 'Switch to Team plan',\n // ENG-9006: the two-option variant Claude Code shipped ~2026-08-17. Filed\n // with a wedged pane and four unanswered customer Telegram messages queued\n // behind it.\n 'Upgrade your plan',\n 'Ask your admin for more usage',\n];\n\n/**\n * Detect Claude Code's usage-limit choice dialog (ENG-8213).\n *\n * On hitting the plan limit, Claude Code blocks the TUI on:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Switch to usage credits\n * 3. Switch to Team plan\n *\n * Enter to confirm · Esc to cancel\n *\n * Nothing answered it, so the session sat on the modal and the agent was\n * unavailable until a human attached to the pane.\n *\n * Matched STRUCTURALLY: a KNOWN option (USAGE_LIMIT_KNOWN_OPTIONS) must appear\n * as a numbered option ROW (`<indent>[❯ ]<digit>. <text>`), not merely\n * somewhere in the capture, and the modal's confirm affordance must be present.\n *\n * The anchor was originally the BILLING rows specifically, which silently made\n * recognition subscription-only and let the Team seat spend-limit variant fall\n * through to a bare Enter. See USAGE_LIMIT_KNOWN_OPTIONS. The structural\n * requirement below is unchanged and is what actually does the safety work; only\n * the vocabulary widened.\n *\n * The looser \"billing phrase anywhere && 'Enter to confirm'\" version was\n * actively dangerous, and in the opposite direction to the bug this fixes.\n * \"Enter to confirm\" is rendered by OTHER dialogs (the MCP confirm among\n * them), and an agent that merely writes the words \"Switch to usage credits\"\n * - discussing a capped teammate, quoting this very file - leaves them in the\n * scrollback. Both true at once and this predicate fires on a pane that is\n * not this dialog. Because sweepDialogs() checks it FIRST and then finds no\n * safe option row, it would return null AND isUnanswerableUsageLimitDialog()\n * would tell every caller to hold the pane: the real dialog never gets\n * answered and the agent wedges. That is the ENG-8194 self-gating shape with\n * a worse blast radius, so the match has to key on layout, not vocabulary.\n */\n/**\n * How far above the \"Enter to confirm\" footer an option row may sit and still\n * count as part of the SAME modal.\n *\n * The real dialog puts its furthest option 4 lines above the footer, so 6\n * carries the whole block with room for a wrap. Deliberately tight: every line\n * of slack here is scrollback that a numbered write-up could occupy, and the\n * cost of being too generous (wedging the pane on a false positive) is worse\n * than the cost of being too tight (failing to answer, which is the status quo\n * this handler improves on).\n */\nconst USAGE_LIMIT_BLOCK_LINES = 6;\n\nfunction optionRowDigit(line: string, label: string): string | null {\n const m = line.match(\n new RegExp(\n String.raw`^[^\\S\\n]*(?:❯[^\\S\\n]*)?(\\d)\\.[^\\S\\n]*` +\n label.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`),\n ),\n );\n return m?.[1] ?? null;\n}\n\n/**\n * Return the modal's own lines, or null when this dialog is not on screen.\n *\n * The match is bound to a REGION, not to the capture as a whole: a known\n * option row and the confirm footer must belong to the same block. Two earlier\n * versions of this predicate were too loose, in the same direction each time:\n *\n * 1. \"billing phrase anywhere && 'Enter to confirm' anywhere\" - satisfied by\n * prose about a capped teammate sitting behind an unrelated dialog.\n * 2. \"billing option ROW anywhere && 'Enter to confirm' anywhere\" - still\n * satisfied by a numbered list in prose (`1. Switch to Team plan`, which\n * is exactly how an agent writes up this very ticket) plus a confirm\n * footer from a different dialog.\n *\n * Both misfire the same way, and it is the dangerous way: sweepDialogs() checks\n * this branch FIRST (so the mcp-servers branch cannot bare-Enter the real\n * modal), so a false positive with no safe option row returns null AND makes\n * isUnanswerableUsageLimitDialog() tell every caller to hold the pane. The\n * dialog actually on screen never gets answered and the agent wedges.\n */\n/**\n * The lines of the FOCUSED modal — the block immediately above the nearest\n * \"Enter to confirm\" footer — or null when no modal is focused.\n *\n * Extracted from usageLimitModalBlock so every structural predicate reads the\n * same region by the same rule. \"Nearest footer\" is load-bearing and was\n * arrived at the hard way: with a newer dialog on screen and an older one still\n * in the scrollback, a backwards search skips the live dialog's footer and\n * matches the STALE one, so we would classify the pane as a dialog that is not\n * focused and key whatever actually is.\n */\nfunction focusedModalBlock(screen: string, blockLines: number): string[] | null {\n const lines = screen.split('\\n');\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i]!.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n }\n if (footer < 0) return null;\n return lines.slice(Math.max(0, footer - blockLines), footer);\n}\n\n/**\n * The MCP-servers dialog's own option rows.\n *\n * New MCP servers found in .mcp.json\n * ❯ 1. Use this and all future MCP servers in this project\n * 2. Continue without using these MCP servers\n *\n * Enter to confirm · Esc to cancel\n */\nconst MCP_DIALOG_OPTIONS: readonly string[] = [\n 'Use this and all future MCP servers in this project',\n 'Continue without using these MCP servers',\n];\n\n/**\n * Detect the MCP-servers dialog STRUCTURALLY.\n *\n * This branch used to read `screen.includes('Enter to confirm') &&\n * screen.includes('MCP')`, which is not a detector — it is a catch-all. Every\n * agent pane carries the string \"MCP\" somewhere (`Connected to MCP server:\n * slack` is printed at startup and never scrolls out of a quiet pane), so the\n * condition reduced to \"any focused confirm dialog at all\", and the action is a\n * BARE ENTER on whatever row the cursor sits on.\n *\n * That is not a hypothetical. It is the mechanism behind the acquire-intelligence\n * wedge on 2026-08-19: the spend-limit modal was not recognised by the\n * usage-limit branch, fell to this one, and got an Enter on the pre-selected\n * \"Stop and wait for limit to reset\". ENG-8213's fix for the SAME hazard was to\n * order the usage-limit branch first — correct, and only a fix for the one\n * dialog anybody had thought of.\n *\n * It also silently defeats the ENG-9006 backstop: an unclaimed modal cannot be\n * reported as unclaimed if this branch claims everything. Tightening here is\n * what lets an unrecognised dialog reach `isUnclaimedConfirmDialog` and become\n * visible instead of being blindly confirmed.\n */\nexport function isMcpServersDialogVisible(screen: string): boolean {\n const block = focusedModalBlock(screen, USAGE_LIMIT_BLOCK_LINES);\n if (!block) return false;\n return block.some((l) => MCP_DIALOG_OPTIONS.some((label) => optionRowDigit(l, label) !== null));\n}\n\n/**\n * ENG-9224 — the SAME dialog after Claude Code 2.1.237 replaced the widget.\n *\n * `isMcpServersDialogVisible` above matches a numbered option list. 2.1.237\n * ships a checkbox multi-select instead, with neither the digits nor either of\n * the `MCP_DIALOG_OPTIONS` labels:\n *\n * 3 new MCP servers found in this project\n * Select any you wish to enable.\n *\n * MCP servers may execute code or access system resources. All tool calls\n * require approval. Learn more in the MCP documentation.\n *\n * ❯ [✓] augmented\n * [✓] direct-chat\n * [✓] composio_googlesheets\n * Space to select · Enter to confirm · Esc to reject all\n *\n * So the branch stopped firing, agents froze on it, and the presence reaper\n * restarted them into the identical prompt until the breaker paused them. Both\n * customer orgs onboarded on 2026-08-20 died this way.\n *\n * This is the second rewrite of this dialog we have chased: ENG-5375 moved the\n * match onto the option TEXT when 2.1.146 reworded the prompt. Matching on text\n * is what left us exposed when the widget itself changed, so this one keys on\n * STRUCTURE — a header, a checked row, a confirm footer — which survives\n * renaming the servers or reordering the list.\n *\n * ## Why a checked row and not just any row\n *\n * `Enter` accepts the CURRENT selection. If Claude Code ever renders this with\n * nothing pre-selected, confirming it enables nothing: the servers stay unbound,\n * the reaper loop continues, and the log says `Auto-accepted MCP servers` while\n * no progress was made. That is strictly worse than today, because it is\n * invisible.\n *\n * So an all-unchecked dialog is deliberately NOT claimed. It falls through to\n * `isUnclaimedConfirmDialog` and becomes a human's problem — the correct outcome\n * for a state we cannot safely resolve by pressing one key.\n */\nconst MCP_CHECKBOX_HEADER = /new MCP servers? found in this project/i;\n\n/**\n * A CHECKED checkbox row: `❯ [✓] augmented`, ` [x] direct-chat`.\n *\n * `[ ]` is excluded on purpose — see the \"why a checked row\" note above. The\n * optional `❯` mirrors `optionRowDigit`'s handling of the focus caret.\n */\nconst MCP_CHECKED_ROW = /^[^\\S\\n]*(?:❯[^\\S\\n]*)?\\[[✓x]\\][^\\S\\n]*\\S/iu;\n\n/**\n * Wider than `USAGE_LIMIT_BLOCK_LINES` because this dialog puts its header\n * above two lines of prose, a blank line, and one row PER SERVER. Six lines\n * reaches the checkboxes but not the header. Twenty covers a comfortable\n * server count while still binding the match to a region rather than the whole\n * capture — the discipline that keeps this file's detectors from becoming the\n * catch-all described above.\n */\nconst MCP_CHECKBOX_BLOCK_LINES = 20;\n\nexport function isMcpCheckboxDialogVisible(screen: string): boolean {\n const block = focusedModalBlock(screen, MCP_CHECKBOX_BLOCK_LINES);\n if (!block) return false;\n if (!block.some((l) => MCP_CHECKBOX_HEADER.test(l))) return false;\n return block.some((l) => MCP_CHECKED_ROW.test(l));\n}\n\nfunction usageLimitModalBlock(screen: string): string[] | null {\n const lines = screen.split('\\n');\n // ONLY the nearest footer. Scanning further back was the fourth version of\n // this bug: with a newer dialog on screen and an older usage-limit block\n // still in the scrollback, a backwards search skips the live dialog's footer\n // (no billing row) and matches the STALE one - so we would classify the pane\n // as the usage-limit modal and send a digit + Enter into whatever dialog is\n // actually focused. The live dialog is always the last footer; treating this\n // as \"search the capture\" rather than \"read the focused dialog\" is what kept\n // producing near-misses.\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i]!.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n }\n if (footer < 0) return null;\n const block = lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer);\n const hasKnownOptionRow = block.some((l) =>\n USAGE_LIMIT_KNOWN_OPTIONS.some((label) => optionRowDigit(l, label) !== null),\n );\n return hasKnownOptionRow ? block : null;\n}\n\n/**\n * Detect Claude Code's usage-limit choice dialog (ENG-8213).\n *\n * On hitting the plan limit, Claude Code blocks the TUI on:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Switch to usage credits\n * 3. Switch to Team plan\n *\n * Enter to confirm · Esc to cancel\n *\n * ...or, on a Claude Team seat that has hit its individual spend limit:\n *\n * What do you want to do?\n *\n * ❯ 1. Stop and wait for limit to reset\n * 2. Wait here, then continue automatically at Aug 21, 10am\n * 3. Ask your admin for more usage\n *\n * Enter to confirm · Esc to cancel\n *\n * Nothing answered it, so the session sat on the modal and the agent was\n * unavailable until a human attached to the pane.\n */\nexport function isUsageLimitChoiceDialogVisible(screen: string): boolean {\n return usageLimitModalBlock(screen) !== null;\n}\n\n/**\n * Locate the option to press by READING ITS NUMBER off the line that carries\n * the option's text — never by assuming it is option 1.\n *\n * This is the whole safety design (ENG-8213). Several rows on this modal cost\n * money or a human's attention: \"Switch to usage credits\" starts pay-as-you-go\n * spend, \"Switch to Team plan\" is a plan change, \"Ask your admin for more\n * usage\" pages a person to raise a budget. Selecting by position would mean a\n * future reordering of this menu silently turns an auto-answer into a purchase.\n * Deriving the digit from the matched text means a reorder just changes which\n * digit we send, and an unrecognised menu yields `null` (answer nothing) rather\n * than a guess.\n *\n * Returns the chosen row's digit (as a tmux key name) and the label it matched,\n * or `null` when no selectable option is present — failing to clear a modal is\n * recoverable; buying credits is not.\n */\nexport function findUsageLimitSafeOption(\n screen: string,\n): { key: string; label: string } | null {\n // Searched inside the modal's own block, so a numbered line elsewhere on the\n // pane can never supply the digit we are about to press.\n const block = usageLimitModalBlock(screen);\n if (!block) return null;\n // Preference order comes from USAGE_LIMIT_SAFE_OPTIONS, not from row order:\n // the OUTER loop is the preference list so `Wait here, then continue\n // automatically` wins wherever it sits on the menu. Iterating rows first\n // would make the answer depend on Claude Code's row ordering, which is the\n // select-by-position mistake this function exists to avoid — just one level\n // up.\n for (const label of USAGE_LIMIT_SAFE_OPTIONS) {\n for (const line of block) {\n const digit = optionRowDigit(line, label);\n if (digit !== null) return { key: digit, label };\n }\n }\n return null;\n}\n\n/** The digit alone, as before. */\nexport function findUsageLimitSafeOptionKey(screen: string): string | null {\n return findUsageLimitSafeOption(screen)?.key ?? null;\n}\n\n/**\n * How many option rows a two-choice consent dialog can span. Wider than the\n * rows themselves so the block still reaches the cursor row when Claude Code\n * pads the modal with blank lines (it does, between the prose and the options).\n */\nconst CONSENT_BLOCK_LINES = 12;\n\n/** Index of the LAST element satisfying `pred`, or -1. */\nfunction lastIndexWhere<T>(xs: readonly T[], pred: (x: T) => boolean): number {\n for (let i = xs.length - 1; i >= 0; i--) {\n if (pred(xs[i]!)) return i;\n }\n return -1;\n}\n\n/**\n * True when `line` is a COMPLETE option row for `label` — optionally preceded by\n * the `❯` cursor and/or a `N.` digit, and carrying nothing else.\n *\n * A substring test is not good enough, and the bypass dialog is exactly where\n * that bites. Its detector is `includes('Yes, I accept') && includes('Bypass\n * Permissions')`, and both strings show up in ordinary prose — an agent that\n * reads a manager log, or discusses this very incident, puts them in its own\n * transcript. A substring match would then find the prose line, measure the\n * cursor distance to it, and key that many Downs into whatever dialog is\n * actually focused. Anchoring to the whole row means prose can never be\n * mistaken for a selectable option; at worst we answer nothing.\n *\n * Anchored at both ends deliberately: every label this is called with names a\n * full row ('Yes, I accept', 'Yes, I trust this folder', \"Don't ask me again\"),\n * unlike USAGE_LIMIT_CONTINUE_OPTION, which is a prefix and is matched by\n * findUsageLimitSafeOption's own looser rule.\n */\n/**\n * True when `line` is a menu CURSOR row — the `❯` opens the line (leading\n * whitespace aside) and something follows it.\n *\n * A bare `includes('❯')` is not good enough: the agent's own input prompt\n * renders the same glyph, and so does any prose quoting a menu. Requiring the\n * glyph to START the row is what separates \"this is the selected option\" from\n * \"this line happens to contain an arrow\".\n */\nfunction isCursorRow(line: string): boolean {\n return /^[^\\S\\n]*❯[^\\S\\n]+\\S/.test(line);\n}\n\nfunction isOptionRowFor(line: string, label: string): boolean {\n const escaped = label.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n return new RegExp(\n String.raw`^[^\\S\\n]*(?:❯[^\\S\\n]*)?(?:\\d\\.[^\\S\\n]*)?` +\n escaped +\n String.raw`[^\\S\\n]*$`,\n ).test(line);\n}\n\n/**\n * ENG-9575: keystrokes that select `label` on the FOCUSED modal, for either\n * shape Claude Code renders — or null when the row isn't there.\n *\n * The bug this exists to kill: every branch below used to answer by DIGIT\n * (`['2','Enter']`) or by BARE ENTER, both of which encode an assumption about\n * how the menu is drawn. Claude Code 2.1.250 draws these consent dialogs as an\n * arrow-select list with NO numbered rows, and — this is the part that turns a\n * cosmetic change into an outage — it defaults the cursor to the REFUSING row:\n *\n * ❯ No, exit\n * Yes, I accept\n * Enter to confirm · Esc to cancel\n *\n * A digit is swallowed (verified on 2.1.250: sending `2` does not move the\n * cursor), so `['2','Enter']` confirms whatever is selected — `No, exit`. Claude\n * exits 0, the manager sees a dead session and respawns, and the next spawn\n * lands on the same dialog. On the two hosts provisioned 2026-08-28 that ran\n * 847 and 1,974 times, every agent down from first boot, while the manager\n * logged `Auto-accepted bypass permissions` on each pass.\n *\n * It is self-perpetuating in a way worth naming: Claude records the consent\n * only after a SUCCESSFUL start, so answering it wrongly also prevents the\n * write that would stop it being asked again. One correct answer ends it for\n * good; a host can never get there on its own.\n *\n * Reading the row instead of assuming its position keeps both shapes working\n * and survives a reorder — the same reasoning as findUsageLimitSafeOption,\n * which derives the digit from the matched text rather than trusting row order.\n * Preserves DEFAULT-DENY: an unrecognised menu returns null, so the caller\n * answers nothing rather than keying a row it cannot see.\n */\nexport function selectRowKeys(\n screen: string,\n label: string,\n): readonly string[] | null {\n // FAIL CLOSED without a focused modal, for BOTH shapes. Every one of these\n // dialogs renders `Enter to confirm` (the numbered shape too — see\n // MCP_DIALOG_OPTIONS above), so no footer means no dialog we are entitled to\n // key. The alternative, searching the whole pane, reads lines the agent\n // itself wrote: a transcript listing the options verbatim (\" 2. Yes, I\n // accept\") is a complete option row by any structural test, and answering it\n // types into the REPL.\n const block = focusedModalBlock(screen, CONSENT_BLOCK_LINES);\n if (!block) return null;\n\n // Last match, not first: with an older render of the same dialog still in\n // the block, the live one is the most recent.\n const target = lastIndexWhere(block, (l) => isOptionRowFor(l, label));\n if (target < 0) return null;\n\n // Numbered shape (pre-2.1.250): the row carries its own digit, so press it.\n // Tried first so a host still on an older Claude Code answers as it always\n // has, and so this fix needs no knowledge of which version a host is running.\n const digit = optionRowDigit(block[target]!, label);\n if (digit !== null) return [digit, 'Enter'];\n\n // Arrow-select shape: RELATIVE movement, so we need the menu's own cursor.\n // Only a row whose `❯` STARTS the line is a cursor — prose that merely\n // contains the glyph is not, and neither is the agent's input prompt.\n const cursors: number[] = [];\n for (let i = 0; i < block.length; i++) {\n if (isCursorRow(block[i]!)) cursors.push(i);\n }\n // No cursor means we cannot know what Enter would confirm. Answer nothing:\n // on these dialogs the unknown row is the destructive one, and DEFAULT-DENY\n // is the module's contract.\n if (cursors.length === 0) return null;\n\n // Nearest cursor to the target — option rows are adjacent, so nearest is the\n // menu this row belongs to. A TIE means two candidate menus are equally\n // close and we cannot tell which owns the row; that is ambiguity, and\n // ambiguity is answered with silence rather than a guess.\n let cursor = cursors[0]!;\n let tied = false;\n for (const i of cursors.slice(1)) {\n const d = Math.abs(i - target);\n const best = Math.abs(cursor - target);\n if (d < best) {\n cursor = i;\n tied = false;\n } else if (d === best) {\n tied = true;\n }\n }\n if (tied) return null;\n\n const distance = target - cursor;\n if (distance === 0) return ['Enter'];\n const step = distance > 0 ? 'Down' : 'Up';\n return [...Array<string>(Math.abs(distance)).fill(step), 'Enter'];\n}\n\n/**\n * ENG-9006: the reset moment the modal names, verbatim, or null.\n *\n * The Team seat spend-limit variant renders its auto-continue row as\n *\n * 2. Wait here, then continue automatically at Aug 21, 10am\n *\n * and that tail is the single most authoritative reset string we ever get from\n * Claude Code. Everywhere else we RECONSTRUCT one: the saturated banner often\n * names only an hour (\"resets 1am (UTC)\") and `banner-parser.ts` resolves it to\n * the next occurrence of that hour, which can never be more than 24h out even\n * when the real reset is days away — the ENG-8901 defect, which had operators\n * scheduling against an instant that was three days early.\n *\n * Returned as the RAW STRING on purpose. Parsing it would mean inventing a\n * second date grammar that can drift from the banner parser's, and guessing a\n * timezone the modal does not state. The one consumer is a sentence shown to a\n * human, and \"until Aug 21, 10am\" is exactly as useful to them as a Date would\n * be, while being impossible to get subtly wrong.\n *\n * Bounded so a mis-parse cannot push an arbitrary pane line into a message that\n * reaches a customer.\n */\nexport function findUsageLimitResetHint(screen: string): string | null {\n const block = usageLimitModalBlock(screen);\n if (!block) return null;\n for (const line of block) {\n if (optionRowDigit(line, USAGE_LIMIT_CONTINUE_OPTION) === null) continue;\n const idx = line.indexOf(USAGE_LIMIT_CONTINUE_OPTION);\n const tail = line.slice(idx + USAGE_LIMIT_CONTINUE_OPTION.length).trim();\n // The row reads \"… continue automatically at <when>\". Drop the connective\n // so the caller can compose \"until <when>\" without doubling it up.\n const when = tail.replace(/^at\\s+/i, '').trim();\n if (!when) return null;\n return when.slice(0, 40);\n }\n return null;\n}\n\n/**\n * The usage-limit dialog is on screen but its safe option could not be\n * located, so there is no keystroke we are willing to send (ENG-8213).\n *\n * Callers MUST treat this as \"do not key this pane at all\" and log it: the\n * generic fallbacks (the watchdog's bounded Enter, acceptDialogs' `❯`\n * readiness branch) would otherwise press Enter on whatever row the cursor\n * happens to be sitting on, and on this dialog most of the rows either spend\n * money or page a human.\n */\nexport function isUnanswerableUsageLimitDialog(screen: string): boolean {\n return (\n isUsageLimitChoiceDialogVisible(screen) &&\n findUsageLimitSafeOptionKey(screen) === null\n );\n}\n\n/**\n * Single-pass dialog recognition. Returns the dismissal action for the\n * first recognised dialog on screen, or `null` when no known dialog is\n * visible (including for the login picker, which must never be keyed\n * past — see isLoginPickerVisible).\n *\n * Branch order mirrors the original acceptDialogs() cascade: the theme\n * picker check must run before any generic `❯`-based readiness logic in\n * callers, since picker rows also render with `❯`.\n */\nexport function sweepDialogs(screen: string): DialogAction | null {\n // ENG-8213: FIRST, and it must stay first. The usage-limit dialog renders\n // \"Enter to confirm\", and the mcp-servers branch below fires on\n // `'Enter to confirm' && 'MCP'` — with \"MCP\" trivially present in the\n // scrollback behind the modal, that branch would match this dialog and\n // send a BARE ENTER, confirming whichever row the cursor is on. Two of\n // those three rows are billing actions. Ordering is the guard.\n if (isUsageLimitChoiceDialogVisible(screen)) {\n const safe = findUsageLimitSafeOption(screen);\n // No recognised safe option => answer nothing. isUnanswerableUsageLimitDialog()\n // lets callers detect this state and stop their own fallbacks keying the pane.\n if (!safe) return null;\n return {\n kind: 'usage-limit-choice',\n // The log names the option ACTUALLY picked, not a fixed literal. With two\n // selectable rows now, a constant string would make a fleet sweep unable\n // to tell an agent that will resume by itself from one parked until a\n // human touches it.\n keys: [safe.key, 'Enter'],\n interKeyDelayMs: 300,\n logMessage: `Auto-answered usage-limit choice dialog (picked '${safe.label}')`,\n };\n }\n // ENG-10619: before the generic branches. The dialog renders\n // `Enter to confirm`, and its default row opts the account INTO model\n // training — so nothing below may get the chance to send a bare Enter.\n // Resolve before claiming (see the folder-trust branch): an unresolvable\n // OFF row falls through rather than suppressing later handlers, and\n // isUnclaimedConfirmDialog() still reports the pane.\n const termsKeys = consumerTermsOffKeys(screen);\n if (termsKeys) {\n return {\n kind: 'consumer-terms',\n keys: termsKeys,\n interKeyDelayMs: 300,\n logMessage: \"Auto-answered consumer-terms dialog (picked 'Help improve our AI models: OFF')\",\n };\n }\n if (\n screen.includes('Choose the text style') ||\n (screen.includes('Dark mode') && screen.includes('Light mode'))\n ) {\n return {\n kind: 'theme-picker',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted theme picker',\n };\n }\n // ENG-9575: was a bare Enter. On 2.1.250 this dialog's default row is\n // \"No, exit\", so the bare Enter REFUSED the trust prompt and exited Claude.\n // Latent rather than firing on the hosts that broke (their agent projects\n // were already trusted), but it fires the first time an agent runs in a\n // project directory Claude has not seen — i.e. on every newly provisioned\n // agent. Select the row by name instead.\n //\n // RESOLVE BEFORE CLAIMING. These predicates are whole-pane substring tests,\n // so scrollback or the agent's own prose can match them while a DIFFERENT\n // dialog is focused. Returning null on an unresolved row would cancel the\n // sweep and suppress every branch below — so trust-folder prose sitting\n // behind a real bypass dialog would leave that dialog unanswered, which is\n // the exact wedge this change exists to prevent. Falling through instead\n // keeps the later handlers reachable and lets isUnclaimedConfirmDialog()\n // still report a pane nobody claimed. Same hazard the usage-limit branch\n // documents as the self-gating trap.\n const trustKeys = screen.includes('Yes, I trust this folder')\n ? selectRowKeys(screen, 'Yes, I trust this folder')\n : null;\n if (trustKeys) {\n return {\n kind: 'folder-trust',\n keys: trustKeys,\n interKeyDelayMs: 300,\n logMessage: 'Auto-accepted folder trust',\n };\n }\n // ENG-5364: picks \"Don't ask me again\", which Claude Code persists in config\n // so subsequent resumes skip the dialog entirely.\n // ENG-9575: was ['3','Enter']. The digit is swallowed on 2.1.250's\n // arrow-select shape, leaving Enter to confirm the default row — here that is\n // \"Resume from summary\", which is harmless but is NOT the option that stops\n // the dialog recurring, so the agent re-answered it on every single respawn.\n // Resolve before claiming — see the folder-trust branch above.\n const resumeKeys = isResumeModeDialogVisible(screen)\n ? selectRowKeys(screen, \"Don't ask me again\")\n : null;\n if (resumeKeys) {\n return {\n kind: 'resume-mode',\n keys: resumeKeys,\n interKeyDelayMs: 300,\n logMessage: \"Auto-dismissed resume-mode dialog (picked 'Don't ask me again')\",\n };\n }\n if (screen.includes('I am using this for local development')) {\n return {\n kind: 'dev-channels',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted dev channels',\n };\n }\n // ENG-9224: both shapes of the same dialog — the pre-2.1.237 numbered list\n // and the 2.1.237 checkbox multi-select. Kept in ONE branch, in the position\n // the numbered one already held, so the ordering hazard ENG-8213 fixed (the\n // usage-limit branch must be evaluated first) is untouched by this change.\n if (isMcpServersDialogVisible(screen) || isMcpCheckboxDialogVisible(screen)) {\n return {\n kind: 'mcp-servers',\n keys: ['Enter'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-accepted MCP servers',\n };\n }\n // ENG-9575: was ['2','Enter'] — the incident branch. See selectRowKeys.\n // Resolve before claiming — see the folder-trust branch above. This one\n // matters most: its two substrings are the ones an agent is likeliest to\n // have written itself, and suppressing the session-feedback branch below\n // leaves a rating prompt swallowing the input box (the koda wedge, ENG-6017).\n const bypassKeys =\n screen.includes('Yes, I accept') && screen.includes('Bypass Permissions')\n ? selectRowKeys(screen, 'Yes, I accept')\n : null;\n if (bypassKeys) {\n return {\n kind: 'bypass-permissions',\n keys: bypassKeys,\n interKeyDelayMs: 300,\n logMessage: 'Auto-accepted bypass permissions',\n };\n }\n // ENG-6017: the rating prompt acts on the bare digit — no Enter needed.\n if (isSessionFeedbackDialogVisible(screen)) {\n return {\n kind: 'session-feedback',\n keys: ['0'],\n interKeyDelayMs: 0,\n logMessage: 'Auto-dismissed session-feedback dialog',\n };\n }\n return null;\n}\n\n/**\n * Send a DialogAction's keystrokes to a tmux session, one send-keys call\n * per key with the action's inter-key delay. execFileSync (not execSync)\n * so the session name is an argv entry rather than shell-interpolated.\n */\nexport async function sendDialogKeys(\n tmuxSession: string,\n action: DialogAction,\n): Promise<void> {\n for (let i = 0; i < action.keys.length; i++) {\n if (i > 0 && action.interKeyDelayMs > 0) {\n await new Promise((r) => setTimeout(r, action.interKeyDelayMs));\n }\n execFileSync('tmux', ['send-keys', '-t', tmuxSession, action.keys[i]!], {\n stdio: 'ignore',\n });\n }\n}\n\n/**\n * ENG-9006 criterion 3: the block of a focused confirm-footer dialog that NO\n * handler in this module claimed, or null.\n *\n * This is the backstop for the failure mode that has now recurred three times\n * in three weeks, each time the same way: Claude Code reworded a modal, every\n * predicate above went false, the pane wedged, and the fleet found out because\n * a human looked at a screenshot.\n *\n * 2026-07-28 ENG-8213 \"Switch to usage credits\" / \"Switch to Team plan\"\n * 2026-08-17 ENG-9006 \"Upgrade your plan\" (two-option variant)\n * 2026-08-19 this \"Wait here, then continue automatically at …\"\n * / \"Ask your admin for more usage\" (Team seat spend)\n *\n * Adding each new string is necessary and is not sufficient — the detector is\n * anchored on vendor-authored copy that the vendor keeps changing, so the NEXT\n * rewording is already scheduled. What does not depend on the wording is the\n * shape: a modal is focused (it renders its confirm affordance) and nothing\n * claimed it.\n *\n * DEFAULT-DENY IS PRESERVED. This function keys nothing and returns no action;\n * it exists purely so an unclaimed modal becomes VISIBLE instead of silent.\n * Answering an unrecognised dialog is still forbidden — a future dialog's\n * default row could be destructive, which is the whole reason sweepDialogs()\n * returns null rather than guessing.\n *\n * Deliberately excludes `isUnanswerableUsageLimitDialog`: that state is\n * recognised, is refused on purpose, and already emits its own loud signal.\n * Counting it here too would double-report one pane.\n */\nexport function focusedConfirmDialogBlock(screen: string): string[] | null {\n const lines = screen.split('\\n');\n // The LIVE dialog is the last footer, same rule as usageLimitModalBlock():\n // an older modal still in the scrollback is not what is focused now.\n let footer = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i]!;\n if (line.includes('Enter to confirm')) {\n footer = i;\n break;\n }\n if (isToolPermissionFooter(line)) {\n // Only a footer that sits under a numbered option row counts; see\n // isToolPermissionFooter for why the footer alone is not enough.\n // ...and only when no input box was rendered after it (Obi Wan\n // ceb92dbbc62b): a VERBATIM quote of the whole dialog - an agent reading\n // this ticket, which reproduces eliza's pane - passes both structural\n // checks, but the session is still live, so its input box sits below.\n const block = lines.slice(Math.max(0, i - USAGE_LIMIT_BLOCK_LINES), i + 1);\n const inputBoxAfter = lines.slice(i + 1).some((l) => INPUT_BOX_RULE.test(l.trim()));\n if (!inputBoxAfter && block.some((l) => NUMBERED_OPTION_ROW.test(l))) {\n footer = i;\n break;\n }\n }\n }\n if (footer < 0) return null;\n return lines.slice(Math.max(0, footer - USAGE_LIMIT_BLOCK_LINES), footer + 1);\n}\n\n/**\n * ENG-10357: the footer Claude Code renders under a TOOL-PERMISSION prompt,\n * which is the dialog its dangerous-rm guard raises (and it raises it even with\n * bypass-permissions on):\n *\n * Dangerous rm operation on critical path: /tmp_rows.txt\n *\n * Do you want to proceed?\n * ❯ 1. Yes\n * 2. No\n * Esc to cancel · Tab to amend · ctrl+e to explain\n *\n * It does NOT say \"Enter to confirm\", so before this the whole blocked-prompt\n * stack (monitor, reaper, session gate) read that pane as clear. Five freezes\n * between 11 and 30 Sep (eliza, vigil, alfred, quinn, vera; up to ~15h each)\n * were every one found by a human reading a pane, never by an alert, while the\n * manager logged `decision=healthy` every cycle.\n *\n * Observed variants, both matched: with and without ` · ctrl+e to explain`.\n *\n * ANCHORED ON \"Tab to amend\", NOT \"Esc to cancel\". The usage-limit banner\n * renders `… · esc to cancel` on a line of its own, and it is not this dialog.\n *\n * QUOTES ARE THE HAZARD. Agents working this ticket (or reading an incident\n * write-up) will have this dialog in their own output, sometimes verbatim. The\n * caller requires three things, and each rejects a different quote shape:\n * - the footer STARTS the line -> rejects an inline prose mention\n * - a numbered option row sits above it -> rejects a bare footer line\n * - NO input-box rule is rendered after -> rejects a verbatim multi-line\n * quote, because a live session still shows its input box below it,\n * whereas a parked prompt has replaced the input box\n * A false positive is still only a noisy page, never a keystroke: nothing in\n * this module ANSWERS an unclaimed dialog (default-deny is unchanged).\n */\nexport function isToolPermissionFooter(line: string): boolean {\n return /^\\s*Esc to cancel · Tab to amend\\b/.test(line);\n}\n\n/** The Claude Code input box's top rule, as channel-input-watchdog reads it. */\nconst INPUT_BOX_RULE = /^[─━]{10,}/;\n\n/**\n * ENG-10357: true when the session is parked on a tool-permission prompt: the\n * pane's focused dialog is a permission footer under numbered options, and no\n * input box has been rendered after it (a quoted prompt has one below it).\n */\nexport function isParkedOnToolPermissionPrompt(screen: string): boolean {\n const block = focusedConfirmDialogBlock(screen);\n if (!block) return false;\n const footerLine = block[block.length - 1]!;\n if (!isToolPermissionFooter(footerLine)) return false;\n const lines = screen.split('\\n');\n let footerIdx = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (lines[i] === footerLine) {\n footerIdx = i;\n break;\n }\n }\n if (footerIdx < 0) return false;\n return !lines.slice(footerIdx + 1).some((l) => INPUT_BOX_RULE.test(l.trim()));\n}\n\n/** A numbered option row, e.g. `❯ 1. Yes` or ` 2. No`. */\nconst NUMBERED_OPTION_ROW = /^\\s*(?:❯\\s*)?\\d\\.\\s+\\S/;\n\n/**\n * True when a focused confirm-footer dialog is on screen and no handler in\n * this module claimed it. See focusedConfirmDialogBlock for why this exists.\n */\nexport function isUnclaimedConfirmDialog(screen: string): boolean {\n if (focusedConfirmDialogBlock(screen) === null) return false;\n if (sweepDialogs(screen) !== null) return false;\n // Already has its own signal — see the doc above.\n if (isUnanswerableUsageLimitDialog(screen)) return false;\n return true;\n}\n\n/**\n * Tiny non-cryptographic hash for hash-only logging of channel input\n * (prod logging policy: input may contain PII/secrets, so log hash+len,\n * never content). Shared by the watchdog and the inject-time hygiene.\n */\nexport function simpleTextHash(s: string): string {\n let h = 0;\n for (let i = 0; i < s.length; i++) {\n h = ((h << 5) - h + s.charCodeAt(i)) | 0;\n }\n return h.toString(16);\n}\n\n/**\n * ENG-9932: a pane state that NO keystroke this module is willing to send can\n * clear, and that therefore a session RESTART cannot clear either.\n *\n * The distinction this type draws is the whole point of the ticket. On\n * agt-aws-1 (2026-09-03) eight agents were auto-paused because\n * `mcp-presence-reaper` saw \"declared MCP(s) have no live children\" and\n * restarted the session — three times in ten minutes, into the circuit breaker\n * (ENG-5441). The MCP servers were not broken. Claude Code had never finished\n * booting, because it was sitting on the login picker, so no MCP child had ever\n * been spawned. Every restart re-presented the same modal.\n *\n * `no live MCP children` is therefore a SYMPTOM with (at least) two causes that\n * want opposite remedies:\n *\n * - an MCP server actually died -> restart the session (correct)\n * - the pane is blocked on a modal -> restarting is futile by\n * construction; surface it\n *\n * Nothing distinguished them, so the reaper applied the first remedy to the\n * second cause and the diagnosis landed two subsystems away from the fault.\n */\n/**\n * Every pane state a restart provably cannot recover.\n *\n * A RUNTIME array, with the type DERIVED from it, deliberately. The obvious\n * shape is a bare union plus a hand-written list in the test — but a\n * three-element `UnanswerablePaneKind[]` stays a valid `UnanswerablePaneKind[]`\n * after the union grows to four, so the coverage assertion would keep passing\n * while the new kind shipped untested. A `Record<UnanswerablePaneKind, true>` in\n * the test does not fix it either: `apps/cli/tsconfig.json` excludes\n * `**\\/*.test.ts`, and vitest strips types without checking them, so a\n * compile-time guard written in a test file is never evaluated by anything.\n *\n * Exporting the values and deriving the type means adding a kind here makes the\n * suite's coverage check fail at RUNTIME, which is the only mechanism that\n * actually runs.\n */\nexport const UNANSWERABLE_PANE_KINDS = [\n /** The login picker (ENG-4634). Needs an operator OAuth; no keystroke and no\n * respawn can complete it, and `acceptDialogs()` deliberately never keys\n * past it. This is the state marlow was found in. */\n 'login-picker',\n /** The usage-limit modal is up but no option we are willing to press is on\n * it (ENG-8213). Recognised and refused ON PURPOSE — every remaining row\n * either spends money or pages a human. A respawn re-presents it. */\n 'usage-limit-unanswerable',\n /** A modal is focused and NO handler in this module claimed it (ENG-9006).\n * Unrecognised means neither the channel-input watchdog nor the spawn-time\n * `acceptDialogs()` cascade can answer it, so a respawn lands on the same\n * prompt. This is the branch that catches the NEXT dialog Claude Code adds,\n * which is the one we cannot enumerate in advance. */\n 'unclaimed-modal',\n] as const;\n\nexport type UnanswerablePaneKind = (typeof UNANSWERABLE_PANE_KINDS)[number];\n\n/**\n * ENG-10972 hardening: the kind of the modal that OWNS THE BOTTOM of the pane -\n * the one the session is actually sitting on - or null when none does.\n *\n * `classifyUnanswerablePane` is a whole-screen substring test, and measured on\n * 2026-09-24 it classifies all four of these as blocked: an agent's message that\n * mentions \"Select login method\", a code line containing the picker's option\n * text, and a usage-limit or unknown modal QUOTED mid-transcript with a live input\n * box below it (`focusedConfirmDialogBlock` finds the LAST \"Enter to confirm\"\n * anywhere on screen). The reaper and spawn-dialogs can use it anyway because they\n * carry a corroborating prior (see `BlockedPromptSource` in\n * session-blocked-prompt.ts). A per-poll watchdog sweep has none, so it uses this.\n *\n * Grounded in the real panes, not guessed: a live confirm modal ends on its\n * footer, and the host-captured login picker (eng-9932's LOGIN_PICKER_PANE) ends\n * on an option row. Anything after that - an input box, more transcript - means\n * the session is not parked on it.\n *\n * The kind is read off WHAT owns the last line, never off the whole screen\n * (CodeRabbit, #6485). The first version checked \"some modal owns the bottom\" and\n * then classified the full pane, where the login-picker test runs first - so a\n * usage-limit modal parked at the bottom with \"Select login method\" anywhere above\n * it was reported as a login picker. So:\n * - a confirm footer at the bottom is classified from its own focused block, and\n * the login-picker substring test never runs (the picker has no such footer);\n * - a picker option row at the bottom is a login picker, and nothing else.\n * Even the 7-line focused block can carry a transcript line above a short modal,\n * which is why the confirm branch checks only the usage-limit and unclaimed kinds.\n * The one question it does NOT scope to the block is \"do we answer this dialog\n * ourselves?\" (sweepDialogs): that text can sit above the block (see below).\n *\n * Deliberately NOT folded into `classifyUnanswerablePane`: tightening that would\n * change the reaper's and spawn's behaviour without live-pane evidence that no\n * Claude Code version renders a hint line under its modals. For the watchdog's\n * busy-ack marker a false negative only restores the old generic notice, so this\n * is the safe place to be strict.\n */\nexport function parkedModalKind(screen: string | null | undefined): UnanswerablePaneKind | null {\n if (!screen) return null;\n const lines = screen.split('\\n').filter((l) => l.trim() !== '');\n const last = lines[lines.length - 1] ?? '';\n if (last.includes('Enter to confirm')) {\n const block = focusedConfirmDialogBlock(screen);\n if (!block) return null;\n const text = block.join('\\n');\n if (isUnanswerableUsageLimitDialog(text)) return 'usage-limit-unanswerable';\n // Obi Wan 9307eb1cbdd8: \"is this a dialog we ANSWER?\" is a whole-screen\n // question. sweepDialogs recognises dialogs by text that can sit far above the\n // footer - the real Bypass Permissions pane's is 11+ lines up, outside the\n // 7-line block - so a block-only check turned claimed dialogs into \"unclaimed\"\n // and marked an agent stuck on a prompt acceptDialogs was about to answer.\n // A claimed dialog merely QUOTED above an unknown modal now reads as claimed:\n // a false negative, which only restores the generic busy notice.\n if (sweepDialogs(screen) !== null) return null;\n if (isUnclaimedConfirmDialog(text)) return 'unclaimed-modal';\n return null;\n }\n // ENG-10357 (Obi Wan b0cdec049c2b): the tool-permission prompt does NOT end on\n // its footer - both real captures carry lines below it (eliza: `⎿ Waiting…`, a\n // spinner and the bypass-mode line; vera: queued direct-chat previews). So the\n // \"last line is the footer\" rule above never sees it, and the watchdog cleared\n // the blocked-prompt marker on every poll for exactly the pane that froze vera.\n // What separates a PARKED permission prompt from a QUOTED one is the thing the\n // 2026-09-24 measurement named: a quoted modal has a live input box below it,\n // and a parked one has replaced the input box. So: the last permission footer\n // (structurally checked by focusedConfirmDialogBlock), with no input-box divider\n // after it.\n if (isParkedOnToolPermissionPrompt(screen)) {\n if (sweepDialogs(screen) !== null) return null;\n return 'unclaimed-modal';\n }\n if (/Claude account with subscription|Anthropic Console account|3rd-party platform/.test(last)) {\n return isLoginPickerVisible(screen) ? 'login-picker' : null;\n }\n return null;\n}\n\n/**\n * Classify a pane that a session restart provably cannot recover, or null.\n *\n * ## Why a RECOGNISED, answerable dialog is deliberately not in this set\n *\n * The tempting version of this function returns non-null for any dialog at\n * all. That would be wrong, and in the dangerous direction: it would add a new\n * way to suppress a restart that is genuinely needed.\n *\n * A dialog `sweepDialogs()` claims gets answered by two independent paths — the\n * per-poll `channel-input-watchdog`, and the spawn-time `acceptDialogs()`\n * cascade in persistent-session.ts. The second one means a restart is a real\n * remedy for an answerable dialog: the fresh session hits the same prompt and\n * the cascade keys past it. So an answerable dialog must keep the pre-ENG-9932\n * behaviour and fall through to the restart.\n *\n * Every kind above is the opposite case: no path answers it, in this session or\n * the next. Holding the restart costs nothing that a restart would have gained,\n * and saves the breaker budget that today gets burned discovering that.\n *\n * DEFAULT-DENY IS PRESERVED, and note the direction. This function keys\n * nothing and returns no `DialogAction`; a pane it does not recognise returns\n * null, which means \"carry on with the restart\" — i.e. unrecognised falls\n * through to the EXISTING behaviour, never to a new suppression.\n */\nexport function classifyUnanswerablePane(\n screen: string | null | undefined,\n): UnanswerablePaneKind | null {\n // A pane we could not read is not evidence of a blocked pane. Fail open to\n // the legacy restart path: a tmux hiccup must never become a silent,\n // fleet-wide suppression of MCP recovery.\n if (!screen) return null;\n if (isLoginPickerVisible(screen)) return 'login-picker';\n if (isUnanswerableUsageLimitDialog(screen)) return 'usage-limit-unanswerable';\n if (isUnclaimedConfirmDialog(screen)) return 'unclaimed-modal';\n return null;\n}\n\n/**\n * A CONTENT-FREE fingerprint of the blocking modal: a stable hash plus the\n * block's length — or null when there is no focused modal block.\n *\n * ## Why a hash and not the text (CodeRabbit, PR #5504)\n *\n * The first version of this returned a 200-character excerpt of the pane. The\n * bound was the wrong control. The modal this fires on most often is the\n * UNCLAIMED one, whose content is by definition unknown to us, and the block is\n * simply the six lines above a confirm footer — which on a busy agent is the\n * transcript it was mid-way through writing. Truncating a customer's message to\n * 200 characters still forwards a customer's message, and `onPaneBlocked`\n * persists this into a control-plane alert row, so it would leave the host.\n *\n * This module's own `simpleTextHash` already states the policy this has to\n * follow — \"input may contain PII/secrets, so log hash+len, never content\" —\n * and `channel-input-watchdog` already reports exactly this data, the unclaimed\n * dialog block, as `unclaimedDialogHash`. The decision was made; this just\n * failed to follow it.\n *\n * Nothing diagnostic is lost. The `UnanswerablePaneKind` is the actual\n * diagnosis, and the operator-facing line names the one action that resolves\n * it (`tmux attach`), where the real pane is a keystroke away. The hash adds\n * the thing an excerpt could not: correlation. Eight agents blocked on the same\n * unknown modal share a hash, which is how you tell one wedged agent from a\n * fleet-wide dialog rollout.\n */\nexport function summarizeUnanswerablePane(\n screen: string,\n): { hash: string; length: number } | null {\n const block = focusedConfirmDialogBlock(screen);\n if (!block) return null;\n const flat = block.join('\\n');\n return { hash: simpleTextHash(flat), length: flat.length };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
consumerTermsOffKeys,
|
|
3
3
|
selectRowKeys
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-OCMJ5E6J.js";
|
|
5
5
|
import {
|
|
6
6
|
CLAUDE_CONFIG_DIR_ENV,
|
|
7
7
|
claudeConfigDirFor,
|
|
@@ -266,7 +266,7 @@ async function spawnPairSession(session) {
|
|
|
266
266
|
return { ok: true };
|
|
267
267
|
} catch {
|
|
268
268
|
}
|
|
269
|
-
const { resolveClaudeBinary } = await import("./persistent-session-
|
|
269
|
+
const { resolveClaudeBinary } = await import("./persistent-session-3FT727PQ.js");
|
|
270
270
|
const claudeBin = resolveClaudeBinary();
|
|
271
271
|
const pairEnv = {
|
|
272
272
|
...process.env,
|
|
@@ -734,4 +734,4 @@ export {
|
|
|
734
734
|
startClaudePair,
|
|
735
735
|
submitClaudePairCode
|
|
736
736
|
};
|
|
737
|
-
//# sourceMappingURL=claude-pair-runtime-
|
|
737
|
+
//# sourceMappingURL=claude-pair-runtime-53UOTF6V.js.map
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
safeWriteJsonAtomic,
|
|
67
67
|
setConfigHash,
|
|
68
68
|
tripClass
|
|
69
|
-
} from "../chunk-
|
|
69
|
+
} from "../chunk-GQYD3ESJ.js";
|
|
70
70
|
import {
|
|
71
71
|
getProjectDir as getProjectDir2,
|
|
72
72
|
getReadyTasks,
|
|
@@ -149,7 +149,7 @@ import {
|
|
|
149
149
|
takeZombieDetection,
|
|
150
150
|
toOpencodeModel,
|
|
151
151
|
writeEgressAllowlist
|
|
152
|
-
} from "../chunk-
|
|
152
|
+
} from "../chunk-FNUTMHWV.js";
|
|
153
153
|
import {
|
|
154
154
|
ACCOUNT_ENFORCEMENT_MARKER_FILENAME,
|
|
155
155
|
AnchorSessionClient,
|
|
@@ -258,11 +258,11 @@ import {
|
|
|
258
258
|
subagentActivityAgeSeconds,
|
|
259
259
|
sumTranscriptUsageInWindow,
|
|
260
260
|
transcriptActivityAgeSeconds
|
|
261
|
-
} from "../chunk-
|
|
261
|
+
} from "../chunk-MQ4COFCI.js";
|
|
262
262
|
import {
|
|
263
263
|
classifyUnanswerablePane,
|
|
264
264
|
paneRemedy
|
|
265
|
-
} from "../chunk-
|
|
265
|
+
} from "../chunk-OCMJ5E6J.js";
|
|
266
266
|
import {
|
|
267
267
|
reapOrphanChannelMcps
|
|
268
268
|
} from "../chunk-XWVM4KPK.js";
|
|
@@ -14040,7 +14040,7 @@ var pendingDayRolloverReset = /* @__PURE__ */ new Set();
|
|
|
14040
14040
|
var dayRolloverInboundHold = /* @__PURE__ */ new Map();
|
|
14041
14041
|
var INBOUND_HOLD_EPISODE_GAP_MS = 12e4;
|
|
14042
14042
|
async function channelInboundActivityAgeSecondsFor(codeName) {
|
|
14043
|
-
const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-
|
|
14043
|
+
const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-DEG3SWG3.js");
|
|
14044
14044
|
const newest = newestPendingInboundActivityMtimeMs(dirname11(paneLogPath(codeName)));
|
|
14045
14045
|
if (newest === null) return null;
|
|
14046
14046
|
return Math.max(0, Math.floor((Date.now() - newest) / 1e3));
|
|
@@ -14786,7 +14786,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
|
|
|
14786
14786
|
var lastVersionCheckAt = 0;
|
|
14787
14787
|
var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
14788
14788
|
var lastResponsivenessProbeAt = 0;
|
|
14789
|
-
var agtCliVersion = true ? "0.28.
|
|
14789
|
+
var agtCliVersion = true ? "0.28.1050" : "dev";
|
|
14790
14790
|
function resolveBrewPath(execFileSync3) {
|
|
14791
14791
|
try {
|
|
14792
14792
|
const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
@@ -16421,7 +16421,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
|
|
|
16421
16421
|
if (codeNames.length === 0) return;
|
|
16422
16422
|
void (async () => {
|
|
16423
16423
|
try {
|
|
16424
|
-
const { collectDiagnostics } = await import("../persistent-session-
|
|
16424
|
+
const { collectDiagnostics } = await import("../persistent-session-3FT727PQ.js");
|
|
16425
16425
|
await api.post("/host/heartbeat", {
|
|
16426
16426
|
host_id: hostId,
|
|
16427
16427
|
agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
|
|
@@ -16563,7 +16563,7 @@ async function pollCycleInner() {
|
|
|
16563
16563
|
}
|
|
16564
16564
|
try {
|
|
16565
16565
|
const { detectHostSecurity } = await import("../host-security-KQYYZZVW.js");
|
|
16566
|
-
const { collectDiagnostics } = await import("../persistent-session-
|
|
16566
|
+
const { collectDiagnostics } = await import("../persistent-session-3FT727PQ.js");
|
|
16567
16567
|
const diagCodeNames = [...agentState.persistentSessionAgents];
|
|
16568
16568
|
const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor) : void 0;
|
|
16569
16569
|
let tailscaleHostname;
|
|
@@ -16748,7 +16748,7 @@ async function pollCycleInner() {
|
|
|
16748
16748
|
collectPanelessActivityProbes,
|
|
16749
16749
|
getResponsivenessIntervalMs,
|
|
16750
16750
|
occupancyQualificationClassifications
|
|
16751
|
-
} = await import("../responsiveness-probe-
|
|
16751
|
+
} = await import("../responsiveness-probe-DEG3SWG3.js");
|
|
16752
16752
|
const probeIntervalMs = getResponsivenessIntervalMs();
|
|
16753
16753
|
if (now - lastResponsivenessProbeAt > probeIntervalMs) {
|
|
16754
16754
|
const probeCodeNames = [...agentState.persistentSessionAgents];
|
|
@@ -16865,7 +16865,7 @@ async function pollCycleInner() {
|
|
|
16865
16865
|
collectResponsivenessProbes,
|
|
16866
16866
|
livePendingInboundOldestAgeSeconds,
|
|
16867
16867
|
parkPendingInbound
|
|
16868
|
-
} = await import("../responsiveness-probe-
|
|
16868
|
+
} = await import("../responsiveness-probe-DEG3SWG3.js");
|
|
16869
16869
|
const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-M6AFZWNU.js");
|
|
16870
16870
|
const wedgeNow = /* @__PURE__ */ new Date();
|
|
16871
16871
|
const liveAgents = agentState.persistentSessionAgents;
|
|
@@ -17021,7 +17021,7 @@ async function pollCycleInner() {
|
|
|
17021
17021
|
}
|
|
17022
17022
|
try {
|
|
17023
17023
|
const { scrapeMcpFailedBannerCount } = await import("../pane-mcp-banner-scraper-VR5QGP3J.js");
|
|
17024
|
-
const { probeSessionAuth } = await import("../session-auth-dead-
|
|
17024
|
+
const { probeSessionAuth } = await import("../session-auth-dead-VFRUHVTF.js");
|
|
17025
17025
|
const observations = [];
|
|
17026
17026
|
const pendingCacheCommits = [];
|
|
17027
17027
|
const modelApiErrorReportingOn = hostFlagStore().getBoolean("model-api-error-reporting");
|
|
@@ -21565,7 +21565,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
|
|
|
21565
21565
|
try {
|
|
21566
21566
|
const freshId = rotateSessionForWedge(codeName);
|
|
21567
21567
|
consecutiveWedgeCycles.delete(codeName);
|
|
21568
|
-
const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-
|
|
21568
|
+
const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-DEG3SWG3.js");
|
|
21569
21569
|
const { parked, deadLettered } = parkForRestart(codeName, /* @__PURE__ */ new Date());
|
|
21570
21570
|
const parkNote = parked > 0 ? `, ${parked} inbound parked` : "";
|
|
21571
21571
|
const deadNote = deadLettered > 0 ? `, ${deadLettered} undeliverable dead-lettered` : "";
|
|
@@ -21608,7 +21608,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
|
|
|
21608
21608
|
void api.post("/host/restart-ack", { host_id: hostId, agent_id: agentId, restart_requested_at: requestedAt }).catch((err) => log(`[restart-lane] ack failed for '${codeName}': ${err.message}`));
|
|
21609
21609
|
void (async () => {
|
|
21610
21610
|
try {
|
|
21611
|
-
const { collectDiagnostics } = await import("../persistent-session-
|
|
21611
|
+
const { collectDiagnostics } = await import("../persistent-session-3FT727PQ.js");
|
|
21612
21612
|
await api.post("/host/heartbeat", {
|
|
21613
21613
|
host_id: hostId,
|
|
21614
21614
|
agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
|
|
@@ -21659,7 +21659,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
|
|
|
21659
21659
|
}
|
|
21660
21660
|
try {
|
|
21661
21661
|
const hostId = await getHostId();
|
|
21662
|
-
const { collectDiagnostics } = await import("../persistent-session-
|
|
21662
|
+
const { collectDiagnostics } = await import("../persistent-session-3FT727PQ.js");
|
|
21663
21663
|
await api.post("/host/heartbeat", {
|
|
21664
21664
|
host_id: hostId,
|
|
21665
21665
|
agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
|
|
@@ -22331,7 +22331,7 @@ async function processClaudePairSessions(agents) {
|
|
|
22331
22331
|
// cannot take it for itself — by the time it runs, the write it is looking
|
|
22332
22332
|
// for may already have happened.
|
|
22333
22333
|
snapshotPairConfig
|
|
22334
|
-
} = await import("../claude-pair-runtime-
|
|
22334
|
+
} = await import("../claude-pair-runtime-53UOTF6V.js");
|
|
22335
22335
|
for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
|
|
22336
22336
|
log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
|
|
22337
22337
|
const killed = await killPairSession(pairTmuxSession(pairId));
|
|
@@ -65,9 +65,9 @@ import {
|
|
|
65
65
|
writeDirectChatSessionState,
|
|
66
66
|
writeEgressAllowlist,
|
|
67
67
|
writePersistentClaudeWrapper
|
|
68
|
-
} from "./chunk-
|
|
69
|
-
import "./chunk-
|
|
70
|
-
import "./chunk-
|
|
68
|
+
} from "./chunk-FNUTMHWV.js";
|
|
69
|
+
import "./chunk-MQ4COFCI.js";
|
|
70
|
+
import "./chunk-OCMJ5E6J.js";
|
|
71
71
|
import "./chunk-XWVM4KPK.js";
|
|
72
72
|
import "./chunk-EL5MWGGH.js";
|
|
73
73
|
import "./chunk-DGUM43GV.js";
|
|
@@ -139,4 +139,4 @@ export {
|
|
|
139
139
|
writeEgressAllowlist,
|
|
140
140
|
writePersistentClaudeWrapper
|
|
141
141
|
};
|
|
142
|
-
//# sourceMappingURL=persistent-session-
|
|
142
|
+
//# sourceMappingURL=persistent-session-3FT727PQ.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
paneLogPath
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-FNUTMHWV.js";
|
|
4
|
+
import "./chunk-MQ4COFCI.js";
|
|
5
|
+
import "./chunk-OCMJ5E6J.js";
|
|
6
6
|
import "./chunk-XWVM4KPK.js";
|
|
7
7
|
import "./chunk-EL5MWGGH.js";
|
|
8
8
|
import "./chunk-DGUM43GV.js";
|
|
@@ -802,4 +802,4 @@ export {
|
|
|
802
802
|
readAndResetSlackReplyBindingClassifications,
|
|
803
803
|
readAndResetSlackReplyTargetClassifications
|
|
804
804
|
};
|
|
805
|
-
//# sourceMappingURL=responsiveness-probe-
|
|
805
|
+
//# sourceMappingURL=responsiveness-probe-DEG3SWG3.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sessionTranscriptDir
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-MQ4COFCI.js";
|
|
4
4
|
import "./chunk-DGUM43GV.js";
|
|
5
5
|
|
|
6
6
|
// src/lib/session-auth-dead.ts
|
|
@@ -204,4 +204,4 @@ export {
|
|
|
204
204
|
decideSessionAuthState,
|
|
205
205
|
probeSessionAuth
|
|
206
206
|
};
|
|
207
|
-
//# sourceMappingURL=session-auth-dead-
|
|
207
|
+
//# sourceMappingURL=session-auth-dead-VFRUHVTF.js.map
|