@songsid/agend 2.1.4-beta.9 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-endpoint.js +1 -1
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/antigravity.d.ts +12 -8
  7. package/dist/backend/antigravity.js +189 -42
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +87 -23
  10. package/dist/backend/claude-code.js +361 -46
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +18 -0
  13. package/dist/backend/codex.js +139 -5
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +2 -2
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.js +2 -2
  18. package/dist/backend/grok.js.map +1 -1
  19. package/dist/backend/kiro.d.ts +52 -2
  20. package/dist/backend/kiro.js +267 -12
  21. package/dist/backend/kiro.js.map +1 -1
  22. package/dist/backend/opencode.js +17 -4
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +122 -3
  25. package/dist/backend/types.js +5 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/backend-outage.d.ts +61 -0
  28. package/dist/backend-outage.js +71 -0
  29. package/dist/backend-outage.js.map +1 -0
  30. package/dist/channel/adapters/discord.d.ts +44 -2
  31. package/dist/channel/adapters/discord.js +523 -90
  32. package/dist/channel/adapters/discord.js.map +1 -1
  33. package/dist/channel/adapters/telegram.d.ts +30 -0
  34. package/dist/channel/adapters/telegram.js +221 -19
  35. package/dist/channel/adapters/telegram.js.map +1 -1
  36. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  37. package/dist/channel/agy-mcp-launcher.js +28 -0
  38. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  39. package/dist/channel/ipc-bridge.d.ts +2 -1
  40. package/dist/channel/ipc-bridge.js +33 -4
  41. package/dist/channel/ipc-bridge.js.map +1 -1
  42. package/dist/channel/markdown-chunk.d.ts +42 -0
  43. package/dist/channel/markdown-chunk.js +205 -0
  44. package/dist/channel/markdown-chunk.js.map +1 -0
  45. package/dist/channel/mcp-server.js +34 -5
  46. package/dist/channel/mcp-server.js.map +1 -1
  47. package/dist/channel/mcp-tools.js +5 -3
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/channel/message-queue.js +64 -8
  50. package/dist/channel/message-queue.js.map +1 -1
  51. package/dist/channel/types.d.ts +31 -1
  52. package/dist/classic-channel-manager.d.ts +65 -0
  53. package/dist/classic-channel-manager.js +172 -3
  54. package/dist/classic-channel-manager.js.map +1 -1
  55. package/dist/cli.js +124 -83
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config-validator.js +13 -0
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +4 -0
  60. package/dist/config.js +12 -1
  61. package/dist/config.js.map +1 -1
  62. package/dist/cross-instance-envelope.d.ts +6 -0
  63. package/dist/cross-instance-envelope.js +30 -0
  64. package/dist/cross-instance-envelope.js.map +1 -0
  65. package/dist/daemon.d.ts +491 -14
  66. package/dist/daemon.js +2121 -276
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/doctor.d.ts +41 -0
  69. package/dist/doctor.js +267 -0
  70. package/dist/doctor.js.map +1 -0
  71. package/dist/fleet-context.d.ts +63 -0
  72. package/dist/fleet-context.js.map +1 -1
  73. package/dist/fleet-manager.d.ts +402 -11
  74. package/dist/fleet-manager.js +2594 -422
  75. package/dist/fleet-manager.js.map +1 -1
  76. package/dist/fleet-yaml-slim.d.ts +10 -0
  77. package/dist/fleet-yaml-slim.js +59 -0
  78. package/dist/fleet-yaml-slim.js.map +1 -0
  79. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  80. package/dist/general-knowledge/skills/model-discovery/SKILL.md +63 -16
  81. package/dist/general-knowledge/skills/session-management/SKILL.md +171 -8
  82. package/dist/instance-lifecycle.d.ts +114 -1
  83. package/dist/instance-lifecycle.js +404 -14
  84. package/dist/instance-lifecycle.js.map +1 -1
  85. package/dist/instructions.js +42 -4
  86. package/dist/instructions.js.map +1 -1
  87. package/dist/locale.js +1024 -320
  88. package/dist/locale.js.map +1 -1
  89. package/dist/login-flows.d.ts +91 -0
  90. package/dist/login-flows.js +170 -0
  91. package/dist/login-flows.js.map +1 -0
  92. package/dist/login-manager.d.ts +63 -0
  93. package/dist/login-manager.js +134 -0
  94. package/dist/login-manager.js.map +1 -0
  95. package/dist/network-family.d.ts +18 -0
  96. package/dist/network-family.js +20 -0
  97. package/dist/network-family.js.map +1 -0
  98. package/dist/outbound-handlers.d.ts +16 -1
  99. package/dist/outbound-handlers.js +174 -40
  100. package/dist/outbound-handlers.js.map +1 -1
  101. package/dist/outbound-schemas.d.ts +10 -3
  102. package/dist/outbound-schemas.js +10 -0
  103. package/dist/outbound-schemas.js.map +1 -1
  104. package/dist/pane-input-residue.d.ts +52 -0
  105. package/dist/pane-input-residue.js +107 -0
  106. package/dist/pane-input-residue.js.map +1 -0
  107. package/dist/process-memory.d.ts +6 -0
  108. package/dist/process-memory.js +45 -0
  109. package/dist/process-memory.js.map +1 -1
  110. package/dist/reply-dedup.d.ts +7 -8
  111. package/dist/reply-dedup.js +0 -0
  112. package/dist/reply-dedup.js.map +1 -1
  113. package/dist/restart-progress.d.ts +2 -0
  114. package/dist/restart-progress.js +3 -0
  115. package/dist/restart-progress.js.map +1 -1
  116. package/dist/scheduler/db.d.ts +12 -0
  117. package/dist/scheduler/db.js +59 -0
  118. package/dist/scheduler/db.js.map +1 -1
  119. package/dist/service-installer.d.ts +11 -0
  120. package/dist/service-installer.js +84 -18
  121. package/dist/service-installer.js.map +1 -1
  122. package/dist/settings-api.js +1 -1
  123. package/dist/settings-api.js.map +1 -1
  124. package/dist/setup-wizard.js +2 -2
  125. package/dist/setup-wizard.js.map +1 -1
  126. package/dist/spawn-gate.d.ts +30 -0
  127. package/dist/spawn-gate.js +79 -0
  128. package/dist/spawn-gate.js.map +1 -0
  129. package/dist/steer-capability.d.ts +1 -0
  130. package/dist/steer-capability.js +6 -0
  131. package/dist/steer-capability.js.map +1 -0
  132. package/dist/storm-window.d.ts +83 -0
  133. package/dist/storm-window.js +251 -0
  134. package/dist/storm-window.js.map +1 -0
  135. package/dist/tips.d.ts +44 -0
  136. package/dist/tips.js +355 -0
  137. package/dist/tips.js.map +1 -0
  138. package/dist/tmux-manager.d.ts +16 -0
  139. package/dist/tmux-manager.js +110 -25
  140. package/dist/tmux-manager.js.map +1 -1
  141. package/dist/topic-commands.d.ts +31 -7
  142. package/dist/topic-commands.js +342 -70
  143. package/dist/topic-commands.js.map +1 -1
  144. package/dist/transcript-sources.d.ts +35 -10
  145. package/dist/transcript-sources.js +190 -38
  146. package/dist/transcript-sources.js.map +1 -1
  147. package/dist/types.d.ts +6 -1
  148. package/dist/ui/dashboard.html +55 -32
  149. package/dist/ui/settings.html +200 -60
  150. package/dist/ui/view.html +147 -30
  151. package/dist/usage/format-rich.d.ts +1 -1
  152. package/dist/usage/format-rich.js +28 -24
  153. package/dist/usage/format-rich.js.map +1 -1
  154. package/dist/usage/i18n-keys.d.ts +7 -0
  155. package/dist/usage/i18n-keys.js +34 -0
  156. package/dist/usage/i18n-keys.js.map +1 -0
  157. package/dist/usage/i18n.d.ts +5 -0
  158. package/dist/usage/i18n.js +27 -0
  159. package/dist/usage/i18n.js.map +1 -0
  160. package/dist/usage/providers.d.ts +21 -0
  161. package/dist/usage/providers.js +153 -75
  162. package/dist/usage/providers.js.map +1 -1
  163. package/dist/usage/usage-api.d.ts +11 -3
  164. package/dist/usage/usage-api.js +61 -24
  165. package/dist/usage/usage-api.js.map +1 -1
  166. package/package.json +2 -2
package/dist/daemon.d.ts CHANGED
@@ -6,11 +6,18 @@ import type { CliBackend, InstanceState, InstanceStateSnapshot } from "./backend
6
6
  import { HangDetector } from "./hang-detector.js";
7
7
  import type { TmuxControlClient } from "./tmux-control.js";
8
8
  import type { FleetInstructionsParams } from "./instructions.js";
9
+ import type { SpawnGate } from "./spawn-gate.js";
10
+ import type { StormWindow } from "./storm-window.js";
11
+ import type { BackendOutageView } from "./backend-outage.js";
9
12
  /** Point a resumed CLI at its one backend-native instruction source. */
10
13
  export declare function buildInstructionReloadNotice(binaryName: string, instanceName: string, instanceDir: string): string;
11
14
  export declare const DEFAULT_STUCK_TIMEOUT_MS: number;
12
15
  export declare const DEFAULT_STATE_IDLE_DEBOUNCE_MS = 2000;
13
16
  export declare const DEFAULT_STATE_SAFETY_SWEEP_MS = 60000;
17
+ /** Coalesce one cosmetic-redraw burst before comparing the visible pane. */
18
+ export declare const PERIODIC_REDRAW_PROBE_MS = 25;
19
+ /** A foreground server/port-forward should hand control back or be acknowledged. */
20
+ export declare const DEFAULT_BLOCKING_PROCESS_GRACE_MS: number;
14
21
  /**
15
22
  * Whether two working directories belong to the same project, so a fleet-scoped
16
23
  * decision recorded in one reaches the other. Covers the worktree/checkout
@@ -62,11 +69,13 @@ export declare function writeLastInboundAt(instanceDir: string, timestamp: numbe
62
69
  export declare function renderHandoffMetadata(meta: Record<string, string>): string;
63
70
  /** Headless inactivity timer used by the daemon and unit tests. */
64
71
  export declare class AutoPauseController {
65
- private readonly thresholdMs;
72
+ private thresholdMs;
66
73
  private lastActivityAt;
67
74
  private pausedAt;
68
75
  constructor(thresholdMs: number, lastActivityAt?: number);
69
76
  recordActivity(now?: number): void;
77
+ /** Apply a new idle threshold and start a fresh countdown from this update. */
78
+ reconfigure(thresholdMs: number, now?: number): void;
70
79
  observe(state: InstanceState, now?: number): boolean;
71
80
  markPaused(now?: number): void;
72
81
  markAwake(now?: number): void;
@@ -142,10 +151,13 @@ export declare class PaneStateMachine {
142
151
  * working" and a finished turn would not be seen as idle until the next 60s
143
152
  * safety sweep. Output timestamps are the honest liveness signal here, and
144
153
  * the caller has them.
154
+ * @param opts.forceBusy the backend has positively identified a still-running
155
+ * foreground tool even though its TUI keeps an old ready footer visible.
145
156
  */
146
157
  observe(pane: string, now?: number, opts?: {
147
158
  settled?: boolean;
148
159
  changeAt?: number;
160
+ forceBusy?: boolean;
149
161
  }): InstanceStateSnapshot;
150
162
  /** Record pane motion from tmux control mode without capturing pane content. */
151
163
  recordOutput(now?: number): InstanceStateSnapshot;
@@ -163,6 +175,14 @@ export declare class PendingWorkTracker {
163
175
  recordIdle(now?: number): void;
164
176
  hasPendingWork(): boolean;
165
177
  }
178
+ /**
179
+ * Startup failed because the CLI's backend is unreachable (see backend-outage.ts).
180
+ * The session-id is deliberately KEPT; the fleet schedules a delayed retry.
181
+ */
182
+ export declare class BackendUnreachableStartupError extends Error {
183
+ readonly backend: string;
184
+ constructor(backend: string);
185
+ }
166
186
  /**
167
187
  * One-shot timing gate for the first paste after a CLI reaches its ready
168
188
  * prompt. A TUI that is still completing its first redraw can swallow Enter.
@@ -237,6 +257,26 @@ export declare function extractProxyReplyText(pane: string, opts?: {
237
257
  maxLines?: number;
238
258
  maxChars?: number;
239
259
  }): string | null;
260
+ export interface MalformedClaudeToolCallDetection {
261
+ /** Stable, non-secret fingerprint used to suppress a stale pane fragment. */
262
+ signature: string;
263
+ /** Sanitized reply argument, or null when only a dangling closing tag survived. */
264
+ text: string | null;
265
+ }
266
+ /**
267
+ * Find the narrow Claude XML failure signature reported in #648.
268
+ *
269
+ * This intentionally runs only at the daemon's busy→idle edge (the caller
270
+ * enforces that) and only scans output after the current inbound marker. A
271
+ * partial tool call while Claude is still streaming is therefore never acted
272
+ * on. The extracted argument goes through the same credential redaction as the
273
+ * dead-MCP pane proxy before it can leave the host.
274
+ */
275
+ export declare function detectMalformedClaudeToolCall(pane: string, opts?: {
276
+ inboundMarker?: string;
277
+ maxLines?: number;
278
+ maxChars?: number;
279
+ }): MalformedClaudeToolCallDetection | null;
240
280
  export type InteractivePromptKind = "sudo_password" | "password" | "confirmation" | "press_enter";
241
281
  export interface InteractivePromptDetection {
242
282
  kind: InteractivePromptKind;
@@ -258,6 +298,30 @@ export declare class InteractivePromptDetector {
258
298
  observe(pane: string, now?: number, outputAt?: number): InteractivePromptDetection | null;
259
299
  reset(): void;
260
300
  }
301
+ export interface BlockingProcessDetection {
302
+ activity: string;
303
+ evidence: string;
304
+ blockedForMs: number;
305
+ }
306
+ /**
307
+ * Detect a foreground shell tool which has become a long-lived server.
308
+ *
309
+ * Unlike the generic pane-stuck clock, this clock deliberately survives new
310
+ * output: access logs and `Handling connection` lines are exactly why a
311
+ * foreground server can remain blocked forever without ever looking silent.
312
+ * Once a server marker is seen, the still-running tool identity is the stable
313
+ * signal; the marker may scroll out of the 32-row pane.
314
+ */
315
+ export declare class BlockingProcessDetector {
316
+ private readonly graceMs;
317
+ private activity;
318
+ private evidence;
319
+ private detectedAt;
320
+ private notified;
321
+ constructor(graceMs?: number);
322
+ observe(pane: string, activity: string | null, now?: number): BlockingProcessDetection | null;
323
+ reset(): void;
324
+ }
261
325
  export declare class Daemon extends EventEmitter {
262
326
  private name;
263
327
  private config;
@@ -266,6 +330,10 @@ export declare class Daemon extends EventEmitter {
266
330
  private backend?;
267
331
  private controlClient?;
268
332
  private runtimeIdentity?;
333
+ private spawnGate?;
334
+ private stormWindow?;
335
+ /** Fleet-level "is this CLI's backend down?" memory — see backend-outage.ts. */
336
+ private backendOutage?;
269
337
  private logger;
270
338
  private tmuxSessionName;
271
339
  private tmux;
@@ -275,6 +343,13 @@ export declare class Daemon extends EventEmitter {
275
343
  private guardian;
276
344
  private adapter;
277
345
  private pendingIpcRequests;
346
+ /**
347
+ * Daemon-global request sequence. MCP request ids are only process-local:
348
+ * Claude can have more than one MCP child connected to the same daemon and
349
+ * each starts at 1, so using requestId alone lets one caller steal another's
350
+ * response.
351
+ */
352
+ private fleetRequestSeq;
278
353
  private lastChatId;
279
354
  private lastThreadId;
280
355
  private lastAdapterId;
@@ -282,9 +357,10 @@ export declare class Daemon extends EventEmitter {
282
357
  /** Last activity published to the fleet manager; null when nothing is running. */
283
358
  private currentActivity;
284
359
  private socketSessionNames;
285
- private static tmuxServerCrashTimestamps;
286
- private static tmuxServerPaused;
287
- private static tmuxServerRecoveryTimer;
360
+ /** pid each connected MCP server announced in mcp_ready (or pong); keyed like socketSessionNames. */
361
+ private socketPids;
362
+ private mcpPingSeq;
363
+ private mcpPingWaiters;
288
364
  private healthCheckTimer;
289
365
  private crashCount;
290
366
  private lastCrashAt;
@@ -305,6 +381,12 @@ export declare class Daemon extends EventEmitter {
305
381
  private resolveSpawnSettled;
306
382
  private spawnDepth;
307
383
  private skipResume;
384
+ private startupAborted;
385
+ /** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
386
+ private dialogParkedSince;
387
+ /** Identity of that dialog: its pattern, not its description (two tables may describe one screen differently). */
388
+ private dialogParkedKey;
389
+ private dialogParkedReported;
308
390
  private backgroundSessionRecoveryAttempted;
309
391
  /** Whether the last spawn started a fresh session (not resumed). */
310
392
  isNewSession: boolean;
@@ -317,6 +399,7 @@ export declare class Daemon extends EventEmitter {
317
399
  /** Fallback safety sweep used only when no shared tmux control client exists. */
318
400
  private instanceStateMonitorTimer;
319
401
  private instanceStateIdleTimer;
402
+ private instanceStateOutputProbeTimer;
320
403
  private instanceStateStuckTimer;
321
404
  private instanceStateOutputListener;
322
405
  private instanceStateOutputEventName;
@@ -331,8 +414,12 @@ export declare class Daemon extends EventEmitter {
331
414
  private statePollInFlight;
332
415
  private turnHadInbound;
333
416
  private turnOutboundDelivered;
417
+ /** Successful `reply` specifically; cross-instance tools do not satisfy #648. */
418
+ private turnReplyDelivered;
334
419
  private turnCorrelationId;
335
420
  private turnInboundMarker;
421
+ /** Prevent a visible stale XML fragment from being recovered on later turns. */
422
+ private lastMalformedToolCallSignature;
336
423
  private proxyReplySeq;
337
424
  private autoPauseController;
338
425
  private pauseRequested;
@@ -343,6 +430,35 @@ export declare class Daemon extends EventEmitter {
343
430
  * would silently no-op exactly when we most need to stop feeding the CLI.
344
431
  */
345
432
  private pausePending;
433
+ /**
434
+ * An auth failure was detected and nothing has resolved it yet.
435
+ *
436
+ * Separate from `pausePending` because it must outlive it: the deferred pause
437
+ * clears `pausePending` as soon as it fires, but if that pause is refused the
438
+ * instance is still auth-broken and must not resume raising alarms.
439
+ */
440
+ private authFailureUnresolved;
441
+ /** One login-screen auth report per spawn — the screen persists across polls. */
442
+ private loginScreenReported;
443
+ /** A fatal startup screen (see StartupDialog.fatal) was already reported this run. */
444
+ private fatalStartupReported;
445
+ /**
446
+ * The CLI is parked on a fatal startup screen and every pane write must be
447
+ * refused. This is deliberately separate from the pause that the reported
448
+ * pty_error triggers: the pause is applied by an async handler, and a queued
449
+ * delivery would race it into the modal (where paste+Enter can confirm
450
+ * "Exit and fix manually"). Set synchronously at dialog-match time; cleared
451
+ * when a startup scan reaches a real ready prompt, or on wake().
452
+ */
453
+ private fatalStartupBlocked;
454
+ /** model_error seen mid-turn: notify only if it survives to the idle screen. */
455
+ private pendingModelErrorKey;
456
+ /**
457
+ * Let the next pause() proceed from a stuck pane. Set only for the auth-deferred
458
+ * pause — see the pausePending consumption site for why waiting for idle there
459
+ * is waiting forever.
460
+ */
461
+ private pauseAllowStuck;
346
462
  private pauseWakeState;
347
463
  private pauseWakeTransition;
348
464
  private modelOverride;
@@ -354,6 +470,12 @@ export declare class Daemon extends EventEmitter {
354
470
  * and ⏳/👀/✅ reactions live here). It does NOT cover the other writers that
355
471
  * reach the pane — {@link paneWriteLock} does. */
356
472
  private pasteLock;
473
+ /**
474
+ * Fleet cancellation epoch. Queue entries retain the epoch they arrived with;
475
+ * advancing it invalidates only work received before the cancel, never a new
476
+ * user message which arrives immediately afterwards.
477
+ */
478
+ private deliveryEpoch;
357
479
  /** Mutual exclusion for *every* write into the pane, whichever subsystem it
358
480
  * comes from. See PaneWriteLock for why interleaving is destructive. */
359
481
  private readonly paneWriteLock;
@@ -376,7 +498,9 @@ export declare class Daemon extends EventEmitter {
376
498
  private lastProgressBroadcastAt;
377
499
  private errorMonitorTimer;
378
500
  private readonly interactivePromptDetector;
501
+ private readonly blockingProcessDetector;
379
502
  /** Same 5-min gate the error monitor uses, so a dead MCP server alerts once. */
503
+ private static readonly MCP_PING_TIMEOUT_MS;
380
504
  private static readonly MCP_DEATH_COOLDOWN_MS;
381
505
  private lastMcpDeathNotifiedAt;
382
506
  private mcpDeathNotifiedForPid;
@@ -387,6 +511,16 @@ export declare class Daemon extends EventEmitter {
387
511
  * past this point interrupting the turn costs less than staying mute.
388
512
  */
389
513
  private static readonly MCP_RESTART_MAX_WAIT_MS;
514
+ /**
515
+ * How long the revival restart waits, at the point of firing, for a
516
+ * CLI-respawned MCP server to announce itself.
517
+ *
518
+ * Codex <0.146 tears down and recreates ALL MCP connections on an auth/config
519
+ * change — healthy ones included — so the daemon sees the old pid die while
520
+ * the CLI is fine and a replacement is seconds away. Module load measured
521
+ * ~0.36s, so 2s is roughly 5x headroom without making a genuine loss wait.
522
+ */
523
+ private static readonly MCP_REPLACEMENT_GRACE_MS;
390
524
  /**
391
525
  * Sticky "restart to revive the MCP server as soon as the pane idles" — same
392
526
  * shape as pausePending, because the death is usually detected mid-turn when
@@ -395,6 +529,25 @@ export declare class Daemon extends EventEmitter {
395
529
  */
396
530
  private mcpRestartPending;
397
531
  private mcpRestartStaleTimer;
532
+ /** In-flight replacement grace; also the guard against three triggers stacking. */
533
+ private mcpRestartGraceTimer;
534
+ /**
535
+ * Which trigger the in-flight grace will commit under.
536
+ *
537
+ * The single-timer guard would otherwise SWALLOW a stale_timeout that lands
538
+ * during an idle-gated grace — and because the stale timer is one-shot and
539
+ * already cleared itself, the 30-minute backstop would be gone for good on an
540
+ * instance that never idles, which is the exact case it exists for.
541
+ */
542
+ private mcpRestartGraceTrigger;
543
+ /**
544
+ * A dead-MCP sighting held back once, pending a possible replacement.
545
+ *
546
+ * Deliberately NOT the same field as `mcpDeathNotifiedForPid`: deferring must
547
+ * not consume the once-per-pid budget, or a suspicion that turns out to be a
548
+ * real loss could never be reported at all.
549
+ */
550
+ private mcpDeathDeferredForPid;
398
551
  /** Prevent in-flight monitor callbacks from re-arming after a pause. */
399
552
  private runtimeMonitorsFrozen;
400
553
  private errorWaitingForRecovery;
@@ -421,7 +574,9 @@ export declare class Daemon extends EventEmitter {
421
574
  private lastDetectedErrorType;
422
575
  private static errorPatternKey;
423
576
  private clearErrorRecoveryGate;
424
- constructor(name: string, config: InstanceConfig, instanceDir: string, topicMode?: boolean, backend?: CliBackend | undefined, controlClient?: TmuxControlClient | undefined, rootLogger?: Logger, runtimeIdentity?: FleetInstructionsParams["runtimeIdentity"]);
577
+ constructor(name: string, config: InstanceConfig, instanceDir: string, topicMode?: boolean, backend?: CliBackend | undefined, controlClient?: TmuxControlClient | undefined, rootLogger?: Logger, runtimeIdentity?: FleetInstructionsParams["runtimeIdentity"], spawnGate?: SpawnGate | undefined, stormWindow?: StormWindow | undefined,
578
+ /** Fleet-level "is this CLI's backend down?" memory — see backend-outage.ts. */
579
+ backendOutage?: BackendOutageView | undefined);
425
580
  start(): Promise<void>;
426
581
  /**
427
582
  * Detect a CRASHED MCP server and report it once.
@@ -437,7 +592,22 @@ export declare class Daemon extends EventEmitter {
437
592
  * process would have no client reading it. Only the CLI can restore its own
438
593
  * tools — hence notify, and let the operator decide about restarting.
439
594
  */
595
+ /**
596
+ * An MCP server has connected and announced itself.
597
+ *
598
+ * Beyond the existing session bookkeeping this is the earliest evidence that
599
+ * tools are back — roughly 30s ahead of the next health tick, which is
600
+ * exactly the window #663's false restarts fired in.
601
+ */
602
+ private handleMcpReady;
440
603
  private checkMcpServerAlive;
604
+ /**
605
+ * Whether a dead MCP pid on this backend is more likely a CLI-driven
606
+ * replacement than a real loss. Codex only, and deliberately narrow: it is
607
+ * the backend whose released fix (0.146) confirms it tore down healthy MCP
608
+ * connections alongside the one it meant to restart.
609
+ */
610
+ private suspectsTransientMcpReplacement;
441
611
  /**
442
612
  * Arm an automatic instance restart to revive a dead MCP server. Fires on the
443
613
  * next busy→idle edge — or immediately when the pane is already idle, which is
@@ -446,7 +616,63 @@ export declare class Daemon extends EventEmitter {
446
616
  * force-restarted: see that constant for why staying mute costs more.
447
617
  */
448
618
  private armMcpRestartWhenIdle;
619
+ /**
620
+ * Single chokepoint for all three revival triggers (already_idle, idle_edge,
621
+ * stale_timeout) — and therefore the one place worth re-checking reality.
622
+ *
623
+ * The pending flag is armed by a health tick that saw the OLD pid die, and
624
+ * was previously fired without ever looking again. When a CLI legitimately
625
+ * respawns its own MCP child, the replacement is already serving by the time
626
+ * the turn ends — but the idle edge fires inside the ~30s before the next
627
+ * health tick can stand the request down, restarting a healthy instance. One
628
+ * external user was restarted 18 times in two days this way (#663).
629
+ *
630
+ * Re-reading here is safe for every backend: worst case it clears a pending
631
+ * restart that should not have existed.
632
+ */
449
633
  private fireMcpRestartRequest;
634
+ /**
635
+ * A recovery is already reshaping the CLI: crash-loop handling
636
+ * (healthCheckPaused), a respawn in flight (spawning), or a pause
637
+ * (isPaused / frozen monitors). Each ends in a fresh CLI — and with it a
638
+ * fresh MCP server — or in supervision ending, where a restart on top would
639
+ * fight the recovery. Either way the revival restart is moot.
640
+ */
641
+ private mcpRestartCancelledByRecovery;
642
+ /**
643
+ * Emit a dead-MCP report that was held back as a suspected replacement, once
644
+ * it is clear no replacement is coming. Consumes the once-per-pid budget
645
+ * here — deferring was to avoid spending it on a false alarm, not to drop
646
+ * the incident.
647
+ */
648
+ private reportDeferredMcpDeath;
649
+ /** Socket-layer answer to pingMcpSockets(); records the pid the server reports. */
650
+ private handleMcpPong;
651
+ /** Live IPC sockets of mcp-server processes that announced THIS instance's session. */
652
+ private liveMcpSockets;
653
+ /**
654
+ * MCP liveness with the pid slot as the first opinion and the IPC layer as
655
+ * the authority: a live connection from a server announcing our session is
656
+ * proof that tools are being served, whatever channel.mcp.pid says.
657
+ */
658
+ private mcpServerAlive;
659
+ /** Point the pid slot at the server that is actually serving, so pid-based readers agree with the IPC layer. */
660
+ private repairMcpPidSlot;
661
+ /**
662
+ * Active, LLM-free confirmation: ask each live socket to answer a ping at
663
+ * the socket layer. Only logged — the open connection is already the
664
+ * decision; a missing pong means an older mcp-server build or a wedged
665
+ * process, and a truly dead socket closes on its own and drops out of
666
+ * liveMcpSockets() by the next tick.
667
+ */
668
+ private pingMcpSockets;
669
+ /**
670
+ * Evidence that a server for this instance is serving (mcp_ready, a tool
671
+ * call, a live connection). Stands down any pending revival restart and,
672
+ * if an alarm had already gone out, retracts it.
673
+ */
674
+ private noteMcpProofOfLife;
675
+ private emitMcpRecovered;
450
676
  private clearMcpRestartRequest;
451
677
  private startHealthCheck;
452
678
  /**
@@ -480,6 +706,8 @@ export declare class Daemon extends EventEmitter {
480
706
  private startErrorMonitor;
481
707
  /** Evaluate one pane snapshot. Kept synchronous so state-machine edges are unit-testable. */
482
708
  private evaluateErrorPatterns;
709
+ /** Arm the recovery gates and emit one detected error pattern. */
710
+ private emitErrorPattern;
483
711
  /**
484
712
  * Notification text for a detected pattern. `formatMessage` patterns build it
485
713
  * from the match so the user gets the specifics (e.g. which period and what
@@ -501,12 +729,37 @@ export declare class Daemon extends EventEmitter {
501
729
  * that message, which is what the user asked for anyway.
502
730
  */
503
731
  sendEscape(): Promise<void>;
732
+ /**
733
+ * Drop every not-yet-started pane delivery which predates a user cancel.
734
+ * Promise chains cannot be removed, so entries become generation-checked
735
+ * no-ops. An entry already past its check is allowed to finish its current PTY
736
+ * transaction; the interrupt key still cancels the CLI generation.
737
+ */
738
+ clearPendingDeliveries(fleetEpoch?: number): void;
739
+ private captureDeliveryEpoch;
740
+ private isDeliveryEpochCurrent;
741
+ private queueRawPaste;
742
+ /**
743
+ * Confirm a backend's terminal-level /clear prompt after the channel-level
744
+ * admin confirmation has already succeeded. Poll instead of pasting `/clear\ny`
745
+ * as one buffer: Kiro versions that clear immediately interpret that queued
746
+ * `y` as a brand-new user message.
747
+ */
748
+ private confirmBackendClearDialog;
504
749
  /** Send the backend-specific graceful quit command/key sequence. */
505
750
  private sendQuitSequence;
506
751
  stop(): Promise<void>;
507
752
  getHangDetector(): HangDetector | null;
508
753
  getInstanceState(): InstanceState | "paused";
509
754
  getInstanceStateSnapshot(): InstanceStateSnapshot;
755
+ /**
756
+ * Answer a FleetManager state query. Most callers only need the cached state,
757
+ * but lifecycle decisions such as post-reply Cancel retirement need an
758
+ * authoritative pane observation: after startup the last transition can stay
759
+ * "working" even though the CLI has since settled without emitting another
760
+ * control-mode output record.
761
+ */
762
+ private respondToInstanceStateQuery;
510
763
  /** Gracefully stop the CLI while keeping its remain-on-exit tmux window. */
511
764
  /**
512
765
  * Mark the instance to pause as soon as its pane is idle. Used for auth
@@ -533,9 +786,21 @@ export declare class Daemon extends EventEmitter {
533
786
  * here (edge-triggered, then reset) is what makes it at most once per turn.
534
787
  */
535
788
  private maybeProxyReplyOnTurnEnd;
789
+ private isClaudeCodeBackend;
536
790
  /** Relay the pane's final text to the channel, marked as a daemon proxy reply. */
537
791
  private sendProxyReply;
792
+ /** Send a narrowly extracted #648 reply; operator notification is a separate event. */
793
+ private sendRecoveredMalformedReply;
794
+ /** Context-bound reply path shared by dead-MCP and malformed-call recovery. */
795
+ private deliverDaemonReply;
538
796
  private clearInstanceStateIdleTimer;
797
+ private clearInstanceStateOutputProbeTimer;
798
+ /**
799
+ * Periodic status-line hooks can generate `%output` without changing a single
800
+ * visible cell. Probe the pane once per burst: a changed capture is real work;
801
+ * an identical ready capture remains idle and emits no state edge.
802
+ */
803
+ private scheduleInstanceStateOutputProbe;
539
804
  private clearInstanceStateStuckTimer;
540
805
  private scheduleInstanceStateStuckDeadline;
541
806
  private scheduleInstanceStateIdleCapture;
@@ -554,16 +819,35 @@ export declare class Daemon extends EventEmitter {
554
819
  * Tell the fleet manager what this instance is doing right now, for the live
555
820
  * progress line on the cancel button.
556
821
  *
557
- * Purely cosmetic — nothing decides anything from it, so it is fine that only
558
- * backends with a transcript feed report at all (claude-code today). For the
559
- * rest the progress line keeps showing just elapsed time.
822
+ * Primarily cosmetic. The foreground-process detector also uses a current
823
+ * shell activity as one of several positive signals, but never makes a state
824
+ * decision from an arbitrary activity label alone.
560
825
  *
561
826
  * Repeats are dropped: the ticker only edits the channel message when the text
562
827
  * changes, and a stream of identical broadcasts would defeat that.
563
828
  */
564
829
  private publishActivity;
565
- /** Effective tool_progress level, hardened against junk config values. */
830
+ /** Effective tool_progress level, hardened against junk config values.
831
+ * Off unless explicitly enabled: an update must never make bubbles start
832
+ * listing tool activity the operator did not ask to broadcast. */
566
833
  private toolProgressLevel;
834
+ /**
835
+ * Apply a tool-progress setting without restarting the CLI session.
836
+ *
837
+ * Drop the current accumulator at the boundary: verbose entries may contain
838
+ * command previews which must not survive a downgrade to standard/off, and a
839
+ * newly enabled level should start with a clean, internally consistent list.
840
+ */
841
+ updateToolProgress(level: InstanceConfig["tool_progress"]): void;
842
+ /** Snapshot the configuration actually owned by this live daemon. */
843
+ getConfigSnapshot(): InstanceConfig;
844
+ /**
845
+ * Apply the whitelisted hot configuration carried by FleetManager IPC.
846
+ * null removes an optional value; invalid/unlisted fields are ignored rather
847
+ * than allowing another client on the per-instance socket to mutate cold
848
+ * process/session settings.
849
+ */
850
+ applyConfigUpdate(value: unknown): void;
567
851
  /**
568
852
  * Accumulate one semantic progress line for the channel bubble and schedule
569
853
  * a coalesced broadcast. Separate from publishActivity on purpose: that one
@@ -596,11 +880,14 @@ export declare class Daemon extends EventEmitter {
596
880
  * agent, or the two drift apart in what the agent is told about replying.
597
881
  */
598
882
  private formatInboundMessage;
883
+ /** Tell the fleet when an already-accepted cross-instance pane write failed. */
884
+ private reportCrossInstanceDeliveryFailure;
599
885
  /**
600
886
  * /steer: interject into the CURRENT turn instead of queueing for idle.
601
887
  *
602
888
  * Differences from pushChannelMessage, and nothing else:
603
- * - serialized on steerLock, not pasteLock — it must overtake, not queue
889
+ * - serialized on steerLock, not pasteLock — it may overtake current work,
890
+ * but never an ordinary delivery already queued ahead of it
604
891
  * - deliverMessage runs with { steer: true }, which takes the busy branch
605
892
  * that pastes immediately (the codex native-queue transaction) instead of
606
893
  * waiting for idle. Verification and silent-loss fallback are the ones
@@ -611,13 +898,20 @@ export declare class Daemon extends EventEmitter {
611
898
  * The steered text goes through formatInboundMessage so the agent sees a
612
899
  * normal inbound message, with a steering notice prepended for context.
613
900
  */
614
- steerMessage(content: string, meta: Record<string, string>): void;
901
+ steerMessage(content: string, meta: Record<string, string>, deliveryEpoch?: number): void;
902
+ /**
903
+ * /btw: submit a separately-labelled side question immediately while asking
904
+ * the agent not to change the active task. Claude's native `/btw` opens a
905
+ * branchAndResume panel whose answer never reaches the channel or transcript,
906
+ * so channel delivery intentionally uses a normal wrapped inbound instead.
907
+ */
908
+ btwMessage(content: string, meta: Record<string, string>, deliveryEpoch?: number): void;
615
909
  /**
616
910
  * Push an inbound channel message to a specific MCP session.
617
911
  * If targetSession is provided, only send to the matching socket.
618
912
  * Otherwise send to the instance's own session (this.name).
619
913
  */
620
- pushChannelMessage(content: string, meta: Record<string, string>, _targetSession?: string): void;
914
+ pushChannelMessage(content: string, meta: Record<string, string>, _targetSession?: string, deliveryEpoch?: number): void;
621
915
  /**
622
916
  * Deliver a single message and drive its status reactions:
623
917
  * ⏳ message_queued — CLI busy; queued locally or by the backend
@@ -635,6 +929,121 @@ export declare class Daemon extends EventEmitter {
635
929
  * serial in both cases so separate PTY writes can never overlap.
636
930
  */
637
931
  private deliverMessage;
932
+ /**
933
+ * The runtime dialogs that hold delivery while on screen (`blocksDelivery`
934
+ * or `holdOnly`). One canonical list and order, shared by the delivery gate
935
+ * and the runtime scanner, so both observers agree on which dialog they see.
936
+ * Startup-only patterns are deliberately excluded: several of them are loose
937
+ * on purpose ("I trust", "Yes, continue") and would match ordinary prose.
938
+ */
939
+ private deliveryBlockingDialogs;
940
+ private static dialogKey;
941
+ /** A dialog counts only when it is the CURRENT interactive region, if the backend can tell; else by pattern. */
942
+ private static dialogMatches;
943
+ /**
944
+ * Tri-state liveness. tmux's own isWindowAlive folds every query failure
945
+ * into `false`, so a transient tmux hiccup would read as "the CLI died" —
946
+ * and callers that clear the session on death must not act on that.
947
+ * getPaneStatus returns null on a failed/ambiguous query and a definite
948
+ * `{alive:false}` for a dead pane; only the latter is evidence.
949
+ */
950
+ private paneLiveness;
951
+ /**
952
+ * Capture the pane and classify it. "unknown" (capture failed) is a distinct
953
+ * outcome on purpose: the resume dialog is silent, so silence cannot vouch
954
+ * for the pane, and treating an unreadable pane as clear is exactly the
955
+ * fail-open that answers a dialog with its default. Callers retry unknown
956
+ * within a bound and then fail loudly.
957
+ */
958
+ private probeBlockingDialog;
959
+ /**
960
+ * Remember how long a blocking dialog has been on screen. Once it has
961
+ * outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
962
+ * dialog is a hold-only variant — report it ONCE so a human answers it.
963
+ * Never presses a key: the whole point is that we do not know which option
964
+ * is safe. Identity is the pattern, so the delivery gate and the runtime
965
+ * scanner (same canonical list) never reset each other's clock.
966
+ */
967
+ private trackDialogParked;
968
+ /**
969
+ * Bottom-anchored readiness for TUIs that drop Enter while busy (see
970
+ * CliBackend.dropsEnterWhileBusy). Output silence is necessary but not
971
+ * sufficient for them: Kiro paints nothing for the whole duration of a shell
972
+ * or MCP tool call (and for each 10s backend retry), yet an Enter arriving
973
+ * then is discarded. Reproduced deterministically: `sleep 9` tool, paste at
974
+ * t+6s → text stranded in the prompt row, next message submitted both.
975
+ * Other backends keep the silence-based gate byte-for-byte.
976
+ */
977
+ private isPaneReadyForDelivery;
978
+ /**
979
+ * Why a pane is not deliverable matters. "busy" may be handed to a native
980
+ * input queue or steered (after its own dialog probe); "dialog" and "unknown"
981
+ * must never be — a paste + Enter into a dialog answers it with its default,
982
+ * and an unreadable pane proves nothing. The silence gate comes first and
983
+ * costs no capture, so a CLI that is simply working never pays for the probe.
984
+ */
985
+ private paneReadinessForDelivery;
986
+ /**
987
+ * Like waitForPaneIdleForDelivery, plus the bottom-anchored prompt check for
988
+ * Enter-dropping TUIs. Polls at a coarse cadence (each poll is a capture-pane)
989
+ * only after the pane is already quiet, so the common fast-idle delivery still
990
+ * costs a single capture.
991
+ */
992
+ private waitForPaneReadyForDelivery;
993
+ /**
994
+ * F3: if an AgEnD message is sitting unsubmitted in the input row (a previous
995
+ * delivery's Enter was dropped), submit it with one bare Enter under the pane
996
+ * lock. Only acts on a positive signature — the `[user:`/`[from:` marker every
997
+ * pasted message carries — never on Kiro's own placeholder hints.
998
+ *
999
+ * Outcomes: "idle" = positively verified nothing stranded (the caller may
1000
+ * paste); "submitted" = an Enter went out for the stranded text; "busy" = the
1001
+ * pane is not at its prompt or could not be read (the caller must wait for
1002
+ * the prompt and run this check AGAIN before pasting — never assume someone
1003
+ * else submitted it); "failed" = the Enter could not be sent (the delivery
1004
+ * must fail rather than paste on top of the stranded text).
1005
+ */
1006
+ private submitStrandedInputIfAny;
1007
+ /**
1008
+ * "stranded": an AgEnD message sits in the input row; "clear": the prompt is
1009
+ * at the bottom with no stranded text; "busy": generating, or the prompt is
1010
+ * not at the bottom (incl. a blank frame); "unknown": the pane could not be
1011
+ * read. Only "clear" is positive evidence.
1012
+ */
1013
+ private strandedInputState;
1014
+ /**
1015
+ * F2: for Enter-dropping TUIs, "some output after Enter" proves nothing — the
1016
+ * turn that made the pane look idle keeps producing output. Confirm the
1017
+ * SUBMISSION instead: the pasted text is gone from the input row ("absent"),
1018
+ * or the TUI's own busy marker is up. "unknown" = the pane could not be read;
1019
+ * it never counts as confirmed.
1020
+ */
1021
+ private pasteResidue;
1022
+ /**
1023
+ * Submission = the legacy idle→busy output signal AND the pasted text has
1024
+ * verifiably left the input row. The first alone is what produced false ✅s:
1025
+ * the turn that made the pane look idle keeps emitting output after our
1026
+ * (dropped) Enter — and an unreadable pane is not a verified absence either.
1027
+ */
1028
+ private confirmSubmittedAfterEnter;
1029
+ /**
1030
+ * While the CLI is parked on a fatal startup screen (see fatalStartupBlocked),
1031
+ * no pane write may happen: paste+Enter into the corrupt-config modal would
1032
+ * confirm "Exit and fix manually" and exit the CLI. The message is failed (❌)
1033
+ * rather than held — the accompanying config_error notification tells the user
1034
+ * what to fix, and holding would only build a queue that floods the pane the
1035
+ * moment the flag clears.
1036
+ */
1037
+ private refuseFatalStartupDelivery;
1038
+ /**
1039
+ * A raw `%output` silence check is authoritative for ordinary TUIs. It is not
1040
+ * for a backend such as agy whose status-line hook periodically redraws an
1041
+ * identical screen. In that case trust an idle pane-state snapshot only when
1042
+ * it was captured after both the latest output and the latest control-client
1043
+ * observation reset.
1044
+ */
1045
+ private isPaneIdleForDelivery;
1046
+ private waitForPaneIdleForDelivery;
638
1047
  /**
639
1048
  * The write half of a delivery: paste → Enter → confirm, with retries.
640
1049
  *
@@ -711,6 +1120,49 @@ export declare class Daemon extends EventEmitter {
711
1120
  private waitForSpawnToSettle;
712
1121
  /** Spawn a CLI window. Returns true if --resume was used successfully. */
713
1122
  private spawnClaudeWindow;
1123
+ /**
1124
+ * Startup budget for this launch: the backend's override (resume-aware), never
1125
+ * below a user-configured startup_timeout_ms; undefined = trySpawn's default.
1126
+ */
1127
+ private startupBudgetFor;
1128
+ /** Effective wake budget = max(caller timeout, backend resume override, configured startup_timeout_ms). */
1129
+ wakeBudgetMs(timeoutMs: number): number;
1130
+ /** The fleet-level backend key this instance runs on (matches the lifecycle's backendOf). */
1131
+ private backendKey;
1132
+ /**
1133
+ * After a failed launch, read the pane for the backend's fleet-wide outage
1134
+ * text (e.g. kiro's `dispatch failure (timeout) … kiro.dev`) and record it.
1135
+ * The lifecycle only learns about outages from RUNNING instances; during a
1136
+ * post-update herd nothing is running yet, so the startup path must report
1137
+ * what it sees or the first instance in would clear its session for nothing.
1138
+ */
1139
+ private noteStartupPaneForBackendOutage;
1140
+ /**
1141
+ * Fail the startup WITHOUT touching the session while the backend is known to
1142
+ * be down. The failed window is torn down first: the alternative — a live but
1143
+ * never-ready CLI left behind while the daemon reports `crashed` — would keep
1144
+ * the health monitor from ever seeing a dead pane and retrying.
1145
+ */
1146
+ private failStartupIfBackendUnreachable;
1147
+ /**
1148
+ * Crash-respawn ran into the backend outage: the ordinary path would leave
1149
+ * `crashed` + a paused monitor + nothing scheduled (and count it as a crash).
1150
+ * Hand the instance to the fleet instead — the lifecycle stops this daemon
1151
+ * and the fleet's delayed startup retry re-creates it later with the session
1152
+ * intact. Returns true when someone took the hand-off.
1153
+ */
1154
+ private handOffBackendUnreachableRespawn;
1155
+ /**
1156
+ * Dispose a Daemon whose start() rejected. The lifecycle registers a daemon
1157
+ * only after start() resolves, so nothing else owns this object — and by the
1158
+ * time the spawn fails it has already bound its IPC server (channel.sock),
1159
+ * written daemon.pid and possibly created a window. Left alone, each failed
1160
+ * start leaks one live-but-unreachable server handle (the next start unlinks
1161
+ * the socket path and binds a new one), multiplied by the automatic startup
1162
+ * retries. Idempotent, and fast: no graceful CLI quit, the CLI never reached
1163
+ * its prompt.
1164
+ */
1165
+ abortStartup(): Promise<void>;
714
1166
  /** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
715
1167
  private killProcessTree;
716
1168
  /**
@@ -720,9 +1172,34 @@ export declare class Daemon extends EventEmitter {
720
1172
  * Returns true if CLI is ready, false if it failed or got stuck.
721
1173
  */
722
1174
  private trySpawn;
1175
+ private trySpawnInsideGate;
1176
+ /**
1177
+ * The last leg of a spawn: confirm the pane is not positively dead, then run
1178
+ * the dialog scan. The old pre-check used tmux's isWindowAlive(), which folds
1179
+ * every query failure into `false` — so one transient tmux error right here
1180
+ * read as "the CLI died", the caller abandoned --resume and cleared the
1181
+ * session, before the tri-state scanner ever got a look. Only a POSITIVE
1182
+ * dead pane fails; an unknown liveness is the scanner's problem (bounded,
1183
+ * session-preserving).
1184
+ */
1185
+ private finishStartupScan;
723
1186
  /**
724
- * Repeatedly check pane content, dismiss any confirmation dialogs,
725
- * and return true once CLI reaches a ready prompt.
1187
+ * Poll the pane within a wall-clock budget, dismiss the confirmation dialogs
1188
+ * the backend knows, and return true once the CLI shows a ready prompt with
1189
+ * no dialog on screen for two consecutive polls.
1190
+ *
1191
+ * Polls are spaced by `pollMs` and every key sequence / render wait counts
1192
+ * against the budget: Claude paints its session-resume prompt only after
1193
+ * loading the session, i.e. AFTER the first quiet moment, and three
1194
+ * back-to-back captures used to miss it — the scan then "assumed ok", the
1195
+ * `❯` inside the dialog satisfied the ready pattern, and the instance sat on
1196
+ * the dialog with deliveries free to press Enter into it.
1197
+ *
1198
+ * Only hard evidence returns false: a dead window or "command not found".
1199
+ * A transient capture failure is logged and retried; exhausting the budget is
1200
+ * logged at WARN and returns true — false makes the caller abandon --resume
1201
+ * and clear the session, which is the loss this code exists to prevent. The
1202
+ * delivery gate keeps refusing pastes while a blocking dialog is visible.
726
1203
  */
727
1204
  private dismissDialogsUntilReady;
728
1205
  /**