@songsid/agend 2.1.10-beta.1 → 2.1.10-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/backend/antigravity.js +2 -1
  4. package/dist/backend/antigravity.js.map +1 -1
  5. package/dist/backend/kiro.d.ts +81 -3
  6. package/dist/backend/kiro.js +362 -29
  7. package/dist/backend/kiro.js.map +1 -1
  8. package/dist/backend/types.d.ts +10 -0
  9. package/dist/backend/types.js +13 -0
  10. package/dist/backend/types.js.map +1 -1
  11. package/dist/channel/ipc-bridge.js +10 -3
  12. package/dist/channel/ipc-bridge.js.map +1 -1
  13. package/dist/channel/mcp-server.d.ts +1 -8
  14. package/dist/channel/mcp-server.js +1 -0
  15. package/dist/channel/mcp-server.js.map +1 -1
  16. package/dist/cli.d.ts +1 -1
  17. package/dist/cli.js +81 -6
  18. package/dist/cli.js.map +1 -1
  19. package/dist/daemon.d.ts +45 -0
  20. package/dist/daemon.js +148 -21
  21. package/dist/daemon.js.map +1 -1
  22. package/dist/fleet-manager.d.ts +25 -1
  23. package/dist/fleet-manager.js +87 -13
  24. package/dist/fleet-manager.js.map +1 -1
  25. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  26. package/dist/instance-lifecycle.d.ts +16 -5
  27. package/dist/instance-lifecycle.js +31 -20
  28. package/dist/instance-lifecycle.js.map +1 -1
  29. package/dist/locale.js +8 -0
  30. package/dist/locale.js.map +1 -1
  31. package/dist/logger.js +19 -16
  32. package/dist/logger.js.map +1 -1
  33. package/dist/muse-usage-relay.js +3 -2
  34. package/dist/muse-usage-relay.js.map +1 -1
  35. package/dist/node-warnings.d.ts +1 -0
  36. package/dist/node-warnings.js +26 -0
  37. package/dist/node-warnings.js.map +1 -0
  38. package/dist/pause-marker.js +3 -2
  39. package/dist/pause-marker.js.map +1 -1
  40. package/dist/private-dir.d.ts +54 -0
  41. package/dist/private-dir.js +140 -0
  42. package/dist/private-dir.js.map +1 -0
  43. package/dist/service-installer.d.ts +46 -0
  44. package/dist/service-installer.js +224 -4
  45. package/dist/service-installer.js.map +1 -1
  46. package/dist/ui/settings.html +2 -2
  47. package/package.json +1 -1
  48. package/templates/systemd.service.ejs +17 -1
package/dist/daemon.d.ts CHANGED
@@ -47,6 +47,13 @@ export declare const DEFAULT_STATE_SAFETY_SWEEP_MS = 60000;
47
47
  export declare const PERIODIC_REDRAW_PROBE_MS = 25;
48
48
  /** Do not turn a streaming Codex turn into one capture-pane per burst. */
49
49
  export declare const STRUCTURED_IDLE_PROBE_MS = 500;
50
+ /**
51
+ * An instance publishing more than this many execution-state edges inside the
52
+ * window is flapping; its edges stop being logged one by one (#1116). One
53
+ * flapping muse wrote 89,532 of the fleet's 91,000 state-change lines.
54
+ */
55
+ export declare const STATE_EDGE_LOG_BURST = 10;
56
+ export declare const STATE_EDGE_LOG_WINDOW_MS = 60000;
50
57
  /** How long an unrecognised-but-empty composer must stay unchanged before it counts as idle (#978). */
51
58
  export declare const UNKNOWN_LAYOUT_STABLE_MS = 10000;
52
59
  /** A foreground server/port-forward should hand control back or be acknowledged. */
@@ -472,6 +479,15 @@ export declare class Daemon extends EventEmitter {
472
479
  /** Generation/signature fence for safety dialogs while their screen is still visible. */
473
480
  private autoResolvedDialogKey;
474
481
  private autoResolvedDialogGeneration;
482
+ /**
483
+ * (#1103) Proof that THIS spawn's Codex went onto Luna Reserve: AgEnD saw (and
484
+ * answered) the usage-limit menu that codex shows when it switches itself to
485
+ * the reserve. `count` is how many usage-limit lines the pane held at that
486
+ * moment; only occurrences up to it are the text of the incident we already
487
+ * handled. A later, larger count is a NEW hit (the reserve ran out too).
488
+ * Keyed by spawn generation, so a respawn starts without any proof.
489
+ */
490
+ private codexReserveAck;
475
491
  private backgroundSessionRecoveryAttempted;
476
492
  /** Whether the last spawn started a fresh session (not resumed). */
477
493
  isNewSession: boolean;
@@ -488,6 +504,10 @@ export declare class Daemon extends EventEmitter {
488
504
  /** Consecutive structural idle captures for a noisy, periodically-redrawing
489
505
  * pane. Two are required before a working -> idle transition. */
490
506
  private instanceStatePeriodicIdleConfirmations;
507
+ /** Recent execution-state edge times, for the flap log limiter (#1116). */
508
+ private stateEdgeTimes;
509
+ private stateEdgesNotLogged;
510
+ private stateEdgeSummaryAt;
491
511
  /** #978: last unrecognised-but-empty-composer capture and since when it has been unchanged. */
492
512
  private unknownLayoutPaneKey;
493
513
  private unknownLayoutStableSince;
@@ -693,6 +713,15 @@ export declare class Daemon extends EventEmitter {
693
713
  * notice is scrollback, not a reason to pause and send /quit.
694
714
  */
695
715
  private isCodexLivePaneSnapshot;
716
+ /** The Codex "usage limit reached" pause pattern (the only one the reserve proof applies to). */
717
+ private static isCodexUsageLimitPause;
718
+ private static countPatternMatches;
719
+ /**
720
+ * Record that the Luna Reserve menu is (or was just) on screen, for the
721
+ * usage-limit decision in evaluateErrorPatterns. Called wherever a dialog is
722
+ * matched, before any key is sent.
723
+ */
724
+ private noteCodexReserveDialog;
696
725
  /** Re-check the current pane after an async quota probe before pausing it. */
697
726
  isCodexLivePane(): Promise<boolean>;
698
727
  /** Whether this instance is in a crash loop (3+ consecutive crashes). */
@@ -893,6 +922,14 @@ export declare class Daemon extends EventEmitter {
893
922
  * the pane monitor to capture it would re-create a false idle state.
894
923
  */
895
924
  /**
925
+ * The CLI was replaced under a running instance by one that cannot run it as
926
+ * configured — kiro-cli auto-updating to a release without the instance's
927
+ * UI/engine (#1109). Every respawn would refuse the same way, so stop
928
+ * supervising and say why instead of failing every 30 seconds.
929
+ */
930
+ private stopOnUnsupportedCliRespawn;
931
+ /**
932
+ * Announce that this instance is no longer being supervised. /**
896
933
  * Announce that this instance is no longer being supervised.
897
934
  *
898
935
  * Four health-check exits set `healthCheckPaused = true` and returned without
@@ -1040,6 +1077,14 @@ export declare class Daemon extends EventEmitter {
1040
1077
  pause(reason?: PauseReason): Promise<void>;
1041
1078
  /** Respawn the CLI in the preserved window and block until its prompt is ready. */
1042
1079
  wake(timeoutMs?: number): Promise<void>;
1080
+ /**
1081
+ * One line per edge, until the instance is flapping. The edges themselves are
1082
+ * still published to every observer; only the log is limited, so the next
1083
+ * flap cannot bury the fleet log the way #1116 did. The first suppressed edge
1084
+ * warns once (that is the signal worth reading), and a summary follows every
1085
+ * window while it lasts and at the first edge after it calms.
1086
+ */
1087
+ private logStateEdge;
1043
1088
  private applyInstanceStateSnapshot;
1044
1089
  /**
1045
1090
  * An inbound message is in the CLI — the turn it starts owns the proxy-reply
package/dist/daemon.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { join, dirname, basename, resolve } from "node:path";
2
2
  import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { ensureInstanceDir } from "./private-dir.js";
4
5
  import { createHash, randomBytes, randomUUID } from "node:crypto";
5
6
  import { EventEmitter } from "node:events";
6
7
  import { rotateLogIfNeeded } from "./logger.js";
@@ -18,7 +19,7 @@ import { ContextGuardian } from "./context-guardian.js";
18
19
  import { IpcServer } from "./channel/ipc-bridge.js";
19
20
  import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
20
21
  import { MessageBus } from "./channel/message-bus.js";
21
- import { shellQuote } from "./backend/types.js";
22
+ import { shellQuote, UnsupportedCliError } from "./backend/types.js";
22
23
  import { getTmuxSession } from "./config.js";
23
24
  import { routeToolCall } from "./channel/tool-router.js";
24
25
  import { HangDetector } from "./hang-detector.js";
@@ -97,6 +98,13 @@ export const DEFAULT_STATE_SAFETY_SWEEP_MS = 60_000;
97
98
  export const PERIODIC_REDRAW_PROBE_MS = 25;
98
99
  /** Do not turn a streaming Codex turn into one capture-pane per burst. */
99
100
  export const STRUCTURED_IDLE_PROBE_MS = 500;
101
+ /**
102
+ * An instance publishing more than this many execution-state edges inside the
103
+ * window is flapping; its edges stop being logged one by one (#1116). One
104
+ * flapping muse wrote 89,532 of the fleet's 91,000 state-change lines.
105
+ */
106
+ export const STATE_EDGE_LOG_BURST = 10;
107
+ export const STATE_EDGE_LOG_WINDOW_MS = 60_000;
100
108
  /** How long an unrecognised-but-empty composer must stay unchanged before it counts as idle (#978). */
101
109
  export const UNKNOWN_LAYOUT_STABLE_MS = 10_000;
102
110
  /** A foreground server/port-forward should hand control back or be acknowledged. */
@@ -161,7 +169,7 @@ export function readLastInboundAt(instanceDir, now = Date.now()) {
161
169
  }
162
170
  /** Atomically persist the last real channel inbound timestamp. */
163
171
  export function writeLastInboundAt(instanceDir, timestamp) {
164
- mkdirSync(instanceDir, { recursive: true });
172
+ ensureInstanceDir(instanceDir);
165
173
  const target = join(instanceDir, LAST_INBOUND_FILE);
166
174
  const temp = `${target}.${process.pid}.tmp`;
167
175
  writeFileSync(temp, String(timestamp));
@@ -1020,6 +1028,15 @@ export class Daemon extends EventEmitter {
1020
1028
  /** Generation/signature fence for safety dialogs while their screen is still visible. */
1021
1029
  autoResolvedDialogKey = null;
1022
1030
  autoResolvedDialogGeneration = 0;
1031
+ /**
1032
+ * (#1103) Proof that THIS spawn's Codex went onto Luna Reserve: AgEnD saw (and
1033
+ * answered) the usage-limit menu that codex shows when it switches itself to
1034
+ * the reserve. `count` is how many usage-limit lines the pane held at that
1035
+ * moment; only occurrences up to it are the text of the incident we already
1036
+ * handled. A later, larger count is a NEW hit (the reserve ran out too).
1037
+ * Keyed by spawn generation, so a respawn starts without any proof.
1038
+ */
1039
+ codexReserveAck = null;
1023
1040
  backgroundSessionRecoveryAttempted = false;
1024
1041
  /** Whether the last spawn started a fresh session (not resumed). */
1025
1042
  isNewSession = false;
@@ -1037,6 +1054,10 @@ export class Daemon extends EventEmitter {
1037
1054
  /** Consecutive structural idle captures for a noisy, periodically-redrawing
1038
1055
  * pane. Two are required before a working -> idle transition. */
1039
1056
  instanceStatePeriodicIdleConfirmations = 0;
1057
+ /** Recent execution-state edge times, for the flap log limiter (#1116). */
1058
+ stateEdgeTimes = [];
1059
+ stateEdgesNotLogged = 0;
1060
+ stateEdgeSummaryAt = 0;
1040
1061
  /** #978: last unrecognised-but-empty-composer capture and since when it has been unchanged. */
1041
1062
  unknownLayoutPaneKey = null;
1042
1063
  unknownLayoutStableSince = 0;
@@ -1275,6 +1296,27 @@ export class Daemon extends EventEmitter {
1275
1296
  return false;
1276
1297
  }
1277
1298
  }
1299
+ /** The Codex "usage limit reached" pause pattern (the only one the reserve proof applies to). */
1300
+ static isCodexUsageLimitPause(ep) {
1301
+ return ep.type === "quota" && ep.action === "pause" && ep.message.startsWith("Codex usage limit reached");
1302
+ }
1303
+ static countPatternMatches(pattern, text) {
1304
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
1305
+ return (text.match(new RegExp(pattern.source, flags)) || []).length;
1306
+ }
1307
+ /**
1308
+ * Record that the Luna Reserve menu is (or was just) on screen, for the
1309
+ * usage-limit decision in evaluateErrorPatterns. Called wherever a dialog is
1310
+ * matched, before any key is sent.
1311
+ */
1312
+ noteCodexReserveDialog(dialog, pane) {
1313
+ if (dialog.autoResolutionKey !== "codex-usage-limit-luna-reserve" || this.backend?.binaryName !== "codex")
1314
+ return;
1315
+ const ep = this.backend.getErrorPatterns?.().find(candidate => Daemon.isCodexUsageLimitPause(candidate));
1316
+ const count = ep ? Daemon.countPatternMatches(ep.pattern, pane) : 0;
1317
+ this.codexReserveAck = { generation: this.spawnGeneration, count };
1318
+ this.logger.info({ backend: "codex", evidence: "reserve-menu-seen", usageLimitLines: count }, "codex usage-limit: the Luna Reserve menu was seen — usage-limit text up to this count is stale");
1319
+ }
1278
1320
  /** Re-check the current pane after an async quota probe before pausing it. */
1279
1321
  async isCodexLivePane() {
1280
1322
  if (this.backend?.binaryName !== "codex" || !this.tmux)
@@ -1575,7 +1617,7 @@ export class Daemon extends EventEmitter {
1575
1617
  }
1576
1618
  }
1577
1619
  async start() {
1578
- mkdirSync(this.instanceDir, { recursive: true });
1620
+ ensureInstanceDir(this.instanceDir);
1579
1621
  writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
1580
1622
  this.logger.info(`Starting ${this.name}`);
1581
1623
  // P1: Read crash state from previous run — skip resume if last run was a crash loop
@@ -2519,6 +2561,10 @@ export class Daemon extends EventEmitter {
2519
2561
  this.stormWindow?.markRecovered(this.name);
2520
2562
  }
2521
2563
  catch (err) {
2564
+ if (this.stopOnUnsupportedCliRespawn(err)) {
2565
+ this.healthCheckTimer = null;
2566
+ return;
2567
+ }
2522
2568
  if (!this.handOffBackendUnreachableRespawn(err)) {
2523
2569
  this.logger.error({ err }, `Failed to respawn ${cliLabel} window`);
2524
2570
  }
@@ -2552,6 +2598,21 @@ export class Daemon extends EventEmitter {
2552
2598
  * the pane monitor to capture it would re-create a false idle state.
2553
2599
  */
2554
2600
  /**
2601
+ * The CLI was replaced under a running instance by one that cannot run it as
2602
+ * configured — kiro-cli auto-updating to a release without the instance's
2603
+ * UI/engine (#1109). Every respawn would refuse the same way, so stop
2604
+ * supervising and say why instead of failing every 30 seconds.
2605
+ */
2606
+ stopOnUnsupportedCliRespawn(err) {
2607
+ if (!(err instanceof UnsupportedCliError))
2608
+ return false;
2609
+ this.healthCheckPaused = true;
2610
+ this.logger.error({ reason: err.message }, "Not respawning: the installed CLI cannot run this instance as configured");
2611
+ this.emitSupervisionEnded(err.message, "Install a CLI version that supports this instance's configuration, then start it again.");
2612
+ return true;
2613
+ }
2614
+ /**
2615
+ * Announce that this instance is no longer being supervised. /**
2555
2616
  * Announce that this instance is no longer being supervised.
2556
2617
  *
2557
2618
  * Four health-check exits set `healthCheckPaused = true` and returned without
@@ -2665,6 +2726,7 @@ export class Daemon extends EventEmitter {
2665
2726
  for (const dialog of dialogs) {
2666
2727
  if (!Daemon.dialogMatches(dialog, pane))
2667
2728
  continue;
2729
+ this.noteCodexReserveDialog(dialog, pane);
2668
2730
  if (dialog.blocksDelivery || dialog.holdOnly)
2669
2731
  blockingSeen = dialog;
2670
2732
  if (dialog.holdOnly)
@@ -2852,15 +2914,29 @@ export class Daemon extends EventEmitter {
2852
2914
  const count = countMatches(ep.pattern);
2853
2915
  const seen = this.lastErrorCount.get(key) ?? 0;
2854
2916
  // The generic Codex usage-limit line remains in scrollback after the
2855
- // user selects Luna Reserve. A live composer + Context footer is positive
2856
- // evidence that the pane recovered; baseline the stale occurrence so it
2857
- // cannot reach the destructive quota pause path.
2858
- if (codexLivePane && ep.type === "quota" && ep.action === "pause"
2859
- && ep.message.startsWith("Codex usage limit reached")) {
2860
- if (count > 0)
2917
+ // user selects Luna Reserve. What proves that is NOT a live composer —
2918
+ // codex paints one after a plain hit too, and the old live-composer guard
2919
+ // therefore swallowed every real hit (#1103) — but AgEnD having seen the
2920
+ // Luna Reserve menu in this spawn. Occurrences up to that point are the
2921
+ // incident we already handled; anything beyond is a new hit and goes on to
2922
+ // the lifecycle, whose usage probe decides (available / reserve / exhausted).
2923
+ if (Daemon.isCodexUsageLimitPause(ep)) {
2924
+ const ack = this.codexReserveAck;
2925
+ if (ack && ack.generation === this.spawnGeneration && count <= ack.count) {
2926
+ // The handled incident's lines can scroll out of the capture buffer as
2927
+ // the reserve session talks on. Lower the proof (and the occurrence
2928
+ // baseline) with them: otherwise `count <= ack.count` would keep
2929
+ // swallowing a NEW hit — the reserve running out — until it exceeded
2930
+ // the menu-time count, and it would never reach the lifecycle probe.
2931
+ if (count < ack.count)
2932
+ ack.count = count;
2861
2933
  this.lastErrorCount.set(key, count);
2862
- this.logger.debug("Codex usage-limit text is stale — live reserve pane is running");
2863
- continue;
2934
+ this.logger.debug({ decision: "ignore", evidence: "reserve-menu-seen", count, acknowledged: ack.count }, "codex usage-limit: stale text of the handled Luna Reserve incident");
2935
+ continue;
2936
+ }
2937
+ if (count > seen) {
2938
+ this.logger.info({ decision: "candidate", evidence: ack && ack.generation === this.spawnGeneration ? "new-hit-after-reserve" : "no-reserve-proof", count, livePane: codexLivePane }, "codex usage-limit: new occurrence — handing to the quota probe");
2939
+ }
2864
2940
  }
2865
2941
  if (count <= seen) {
2866
2942
  // Occurrences scrolled out of the capture buffer → lower the baseline
@@ -3632,6 +3708,39 @@ export class Daemon extends EventEmitter {
3632
3708
  this.pauseWakeTransition = null;
3633
3709
  }
3634
3710
  }
3711
+ /**
3712
+ * One line per edge, until the instance is flapping. The edges themselves are
3713
+ * still published to every observer; only the log is limited, so the next
3714
+ * flap cannot bury the fleet log the way #1116 did. The first suppressed edge
3715
+ * warns once (that is the signal worth reading), and a summary follows every
3716
+ * window while it lasts and at the first edge after it calms.
3717
+ */
3718
+ logStateEdge(previous, snapshot) {
3719
+ const now = Date.now();
3720
+ this.stateEdgeTimes.push(now);
3721
+ while (this.stateEdgeTimes.length && this.stateEdgeTimes[0] <= now - STATE_EDGE_LOG_WINDOW_MS)
3722
+ this.stateEdgeTimes.shift();
3723
+ const edges = this.stateEdgeTimes.length;
3724
+ const fields = { previousState: previous, state: snapshot.state, unchangedForMs: snapshot.unchangedForMs };
3725
+ if (edges <= STATE_EDGE_LOG_BURST) {
3726
+ if (this.stateEdgesNotLogged > 0) {
3727
+ this.logger.info({ notLogged: this.stateEdgesNotLogged }, "Instance execution state edges were not logged while it was flapping");
3728
+ this.stateEdgesNotLogged = 0;
3729
+ }
3730
+ this.logger.info(fields, "Instance execution state changed");
3731
+ return;
3732
+ }
3733
+ if (this.stateEdgesNotLogged === 0) {
3734
+ this.stateEdgeSummaryAt = now;
3735
+ this.logger.warn({ edgesInWindow: edges, windowMs: STATE_EDGE_LOG_WINDOW_MS, backend: this.backend?.binaryName }, "Instance execution state is flapping — per-edge logging suppressed; the pane is repainting without a stable idle/working reading");
3736
+ }
3737
+ else if (now - this.stateEdgeSummaryAt >= STATE_EDGE_LOG_WINDOW_MS) {
3738
+ this.stateEdgeSummaryAt = now;
3739
+ this.logger.warn({ notLogged: this.stateEdgesNotLogged, edgesInWindow: edges }, "Instance execution state is still flapping");
3740
+ }
3741
+ this.stateEdgesNotLogged++;
3742
+ this.logger.debug(fields, "Instance execution state changed (flapping, not logged at info)");
3743
+ }
3635
3744
  applyInstanceStateSnapshot(snapshot, pane) {
3636
3745
  const previous = this.instanceState;
3637
3746
  // A live safety menu owns stdin. It may still contain Claude's persistent
@@ -3667,11 +3776,7 @@ export class Daemon extends EventEmitter {
3667
3776
  this.maybeProxyReplyOnTurnEnd(pane);
3668
3777
  }
3669
3778
  if (snapshot.state !== previous) {
3670
- this.logger.info({
3671
- previousState: previous,
3672
- state: snapshot.state,
3673
- unchangedForMs: snapshot.unchangedForMs,
3674
- }, "Instance execution state changed");
3779
+ this.logStateEdge(previous, snapshot);
3675
3780
  this.emit("instance_state", { name: this.name, ...snapshot });
3676
3781
  this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
3677
3782
  if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
@@ -4169,6 +4274,19 @@ export class Daemon extends EventEmitter {
4169
4274
  if (reason === "delivery_idle_gate")
4170
4275
  this.resetFooterFallback();
4171
4276
  if (reason === "output_probe") {
4277
+ // A TUI that repaints without changing a cell (muse parks its cursor
4278
+ // ~18 times a second while idle; Codex's star field) never leaves the
4279
+ // probe a quiet moment, so roughly one probe in five is overtaken. An
4280
+ // idle instance whose stale capture still shows the structural idle
4281
+ // layout has nothing to be called working on: the rule that cosmetic
4282
+ // output must not manufacture an idle -> working edge holds here too.
4283
+ // Without this every overtaken probe published the edge, and the next
4284
+ // clean probe (one confirmation was still banked) published it back —
4285
+ // a pair every ~1.6s for as long as the repaint lasted (#1116). Real
4286
+ // work cannot hide behind this: it changes the layout, the next
4287
+ // output arms another probe, and the debounce capture still fires.
4288
+ if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true)
4289
+ return;
4172
4290
  // A second burst overtook the probe. Do not bless its stale capture as
4173
4291
  // idle: continuous output is the safer working signal, while the next
4174
4292
  // quiet probe/debounce can still prove that it was only repainting.
@@ -5533,10 +5651,18 @@ export class Daemon extends EventEmitter {
5533
5651
  return { state: "unknown" };
5534
5652
  }
5535
5653
  for (const dialog of dialogs) {
5536
- if (Daemon.dialogMatches(dialog, pane)) {
5537
- this.trackDialogParked(dialog);
5538
- return { state: "dialog", dialog };
5539
- }
5654
+ if (!Daemon.dialogMatches(dialog, pane))
5655
+ continue;
5656
+ // Same one-shot fence as the scanners: a safety choice already sent in
5657
+ // this spawn is not what the screen is waiting on any more — the entry
5658
+ // after it (typically a hold) describes it.
5659
+ if (dialog.verifyAfterKeys && dialog.autoResolutionKey
5660
+ && this.autoResolvedDialogGeneration === this.spawnGeneration
5661
+ && this.autoResolvedDialogKey === dialog.autoResolutionKey
5662
+ && dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
5663
+ continue;
5664
+ this.trackDialogParked(dialog);
5665
+ return { state: "dialog", dialog };
5540
5666
  }
5541
5667
  this.trackDialogParked(null);
5542
5668
  return { state: "clear" };
@@ -8245,7 +8371,7 @@ export class Daemon extends EventEmitter {
8245
8371
  // Transient tmux trouble is not evidence about the CLI. Returning false
8246
8372
  // here would clear the session; retry within the budget instead.
8247
8373
  captureFailures++;
8248
- this.logger.warn({ err, captureFailures }, "capture-pane failed during the startup dialog scan — retrying");
8374
+ this.logger.debug({ err, captureFailures }, "capture-pane failed during the startup dialog scan — retrying");
8249
8375
  // Only a POSITIVE dead pane ends the scan; an unanswerable liveness
8250
8376
  // query is the same transient trouble and must not clear the session.
8251
8377
  if ((await this.paneLiveness()) === "dead")
@@ -8277,6 +8403,7 @@ export class Daemon extends EventEmitter {
8277
8403
  && this.autoResolvedDialogKey === dialog.autoResolutionKey)))
8278
8404
  continue;
8279
8405
  lastDialog = dialog;
8406
+ this.noteCodexReserveDialog(dialog, pane);
8280
8407
  cleanReadyPolls = 0;
8281
8408
  // Start the parked clock for every delivery-blocking dialog, exact
8282
8409
  // ones included: if the auto-dismiss keeps failing here, the human