@adhdev/daemon-core 0.9.82-rc.260 → 0.9.82-rc.261

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/index.js CHANGED
@@ -28744,8 +28744,12 @@ var TerminalAdapter = class {
28744
28744
  this.rows = opts.rows ?? import_session_host_core6.DEFAULT_SESSION_HOST_ROWS;
28745
28745
  this.screenDebounceMs = opts.screenChangeDebounceMs ?? 80;
28746
28746
  this.tickIntervalMs = opts.tickIntervalMs ?? 0;
28747
- const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS(pty_transport_exports));
28748
- this.factory = opts.transportFactory ?? new NodePtyTransportFactory2();
28747
+ if (opts.transportFactory) {
28748
+ this.factory = opts.transportFactory;
28749
+ } else {
28750
+ const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS(pty_transport_exports));
28751
+ this.factory = new NodePtyTransportFactory2();
28752
+ }
28749
28753
  this.screen = new TerminalScreen(this.rows, this.cols);
28750
28754
  }
28751
28755
  rows;
@@ -28902,6 +28906,10 @@ var FsmDriver = class {
28902
28906
  specWatcher = null;
28903
28907
  /** Last full FSM evaluation, kept for the debugger. */
28904
28908
  lastFsmEval = null;
28909
+ /** Ring buffer (max 20) of the full FSM evaluation captured at each
28910
+ * transition — the rich pre-transition table that lastFsmEval only keeps
28911
+ * for the single most recent evaluation. Separate from stateHistory. */
28912
+ fsmSnapshotHistory = [];
28905
28913
  subscribe(listener) {
28906
28914
  this.listeners.add(listener);
28907
28915
  return () => {
@@ -28992,6 +29000,9 @@ var FsmDriver = class {
28992
29000
  getStateHistory() {
28993
29001
  return this.stateHistory;
28994
29002
  }
29003
+ getFsmSnapshotHistory() {
29004
+ return this.fsmSnapshotHistory;
29005
+ }
28995
29006
  getSections() {
28996
29007
  try {
28997
29008
  const screen = this.adapter.snapshot();
@@ -29087,7 +29098,7 @@ var FsmDriver = class {
29087
29098
  this.lastFsmEval = ev;
29088
29099
  this.prevScreenLines = currentLines;
29089
29100
  if (ev.fired) {
29090
- this.commitTransition(ev.fired, now);
29101
+ this.commitTransition(ev.fired, now, ev);
29091
29102
  this.emitStateChanged(forceEmit);
29092
29103
  this.scheduleWakeForState();
29093
29104
  return;
@@ -29096,8 +29107,9 @@ var FsmDriver = class {
29096
29107
  this.scheduleWakeForState();
29097
29108
  this.maybeMarkReady();
29098
29109
  }
29099
- commitTransition(fired, now) {
29110
+ commitTransition(fired, now, ev) {
29100
29111
  const from = this.currentStateId;
29112
+ this.pushFsmSnapshot(from, fired, now, ev);
29101
29113
  this.currentStateId = fired.to;
29102
29114
  this.stateEnteredAt = now;
29103
29115
  this.regionLastChangedAt.clear();
@@ -29108,6 +29120,22 @@ var FsmDriver = class {
29108
29120
  });
29109
29121
  LOG.info("FsmDriver", `[${this.specTag()}] ${from} \u2192 ${fired.to} (${fired.label})`);
29110
29122
  }
29123
+ /** Snapshot the full FSM evaluation that produced a transition into the
29124
+ * separate fsmSnapshotHistory ring buffer (max 20). The transitions[]
29125
+ * table is captured by reference — it is freshly built per evaluation in
29126
+ * evaluateFsm and never mutated after, so no clone is needed. */
29127
+ pushFsmSnapshot(from, fired, now, ev) {
29128
+ this.fsmSnapshotHistory.push({
29129
+ stateFrom: from,
29130
+ stateTo: fired.to,
29131
+ at: now,
29132
+ firedTo: fired.to,
29133
+ firedLabel: fired.label,
29134
+ reason: summarizeTransition(fired),
29135
+ transitions: ev.transitions
29136
+ });
29137
+ if (this.fsmSnapshotHistory.length > 20) this.fsmSnapshotHistory.shift();
29138
+ }
29111
29139
  /** Re-derive the visible modal + controls for the current state and emit a
29112
29140
  * state_changed if anything differs from the last emit. */
29113
29141
  emitStateChanged(forceEmit) {
@@ -30140,7 +30168,7 @@ init_logger();
30140
30168
  function stripAnsi3(text) {
30141
30169
  return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
30142
30170
  }
30143
- var SpecCliAdapter = class {
30171
+ var SpecCliAdapter = class _SpecCliAdapter {
30144
30172
  cliType;
30145
30173
  cliName;
30146
30174
  workingDir;
@@ -30165,6 +30193,23 @@ var SpecCliAdapter = class {
30165
30193
  activeInteractivePrompt = null;
30166
30194
  interactivePromptTransport = null;
30167
30195
  claudeTuiPromptCaptureInFlight = false;
30196
+ /**
30197
+ * Wall clock of the first frame on which a held interactive prompt was
30198
+ * observed to have left the screen. Mirrors the approval FSM's
30199
+ * `modalLostAt` hysteresis (see cli-state-engine.ts): claude-cli's TUI
30200
+ * repaints the choice picker as several PTY chunks, so a single frame
30201
+ * with no "Enter to select" footer is not proof the prompt is gone — it
30202
+ * may just be mid-repaint. We only clear the held prompt once it has
30203
+ * been absent across a short grace window. Reset to null the moment the
30204
+ * prompt footer reappears.
30205
+ *
30206
+ * Without this, a choice prompt resolved *directly in the terminal* (the
30207
+ * user picked an option without going through ADHDev's
30208
+ * setInteractivePromptResponse) was never cleared from
30209
+ * `activeInteractivePrompt`, so getStatus() re-emitted the same prompt
30210
+ * forever — the choice-resolve-stuck bug.
30211
+ */
30212
+ interactivePromptLostAt = null;
30168
30213
  jsonLineTail = "";
30169
30214
  exited = false;
30170
30215
  spawned = false;
@@ -30425,6 +30470,11 @@ var SpecCliAdapter = class {
30425
30470
  // transition from the current state with its per-condition match
30426
30471
  // result + countdown — the canonical "why isn't it moving" answer.
30427
30472
  fsm: this.driver.getFsmDebug?.() ?? null,
30473
+ // v4 FSM transition snapshot history (null for v3 specs). The full
30474
+ // pre-transition evaluation table captured at each transition —
30475
+ // answers "why did this rule fire" after the fact, unlike the live
30476
+ // `fsm` field which only reflects the current instant.
30477
+ fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
30428
30478
  // Extended fields
30429
30479
  name: this.cliName,
30430
30480
  status: this.getStatus().status,
@@ -30462,11 +30512,13 @@ var SpecCliAdapter = class {
30462
30512
  if (ev.state.title) {
30463
30513
  LOG.debug("SpecAdapter", `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
30464
30514
  }
30515
+ this.maybeClearResolvedClaudeTuiPrompt();
30465
30516
  this.maybeCaptureClaudeTuiPrompt();
30466
30517
  this.statusCallback?.();
30467
30518
  return;
30468
30519
  case "pty_data":
30469
30520
  this.detectInteractivePromptFromPtyChunk(ev.chunk);
30521
+ this.maybeClearResolvedClaudeTuiPrompt();
30470
30522
  this.maybeCaptureClaudeTuiPrompt();
30471
30523
  try {
30472
30524
  this.ptyDataCallback?.(ev.chunk);
@@ -30499,6 +30551,7 @@ var SpecCliAdapter = class {
30499
30551
  if (!prompt) continue;
30500
30552
  this.activeInteractivePrompt = prompt;
30501
30553
  this.interactivePromptTransport = "stream-json";
30554
+ this.interactivePromptLostAt = null;
30502
30555
  this.statusCallback?.();
30503
30556
  } catch {
30504
30557
  }
@@ -30552,6 +30605,47 @@ var SpecCliAdapter = class {
30552
30605
  }
30553
30606
  return messages;
30554
30607
  }
30608
+ /**
30609
+ * Grace window a held interactive prompt must be absent from the screen
30610
+ * before we treat it as resolved-in-terminal and clear it. claude-cli
30611
+ * repaints the picker across multiple PTY chunks, so a single
30612
+ * footer-less frame is not proof the prompt is gone. Sized in the same
30613
+ * spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
30614
+ */
30615
+ static INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
30616
+ /**
30617
+ * Clear a held interactive prompt once the user has resolved it directly
30618
+ * in the terminal (the choice picker leaves the screen without going
30619
+ * through setInteractivePromptResponse). The approval path already does
30620
+ * this via the FSM's modal-lost hysteresis; the interactive-prompt path
30621
+ * had no equivalent, so a terminal-side answer left activeInteractivePrompt
30622
+ * set and getStatus() re-emitted the same choice modal forever.
30623
+ *
30624
+ * Detection mirrors capture: the claude TUI picker is on-screen exactly
30625
+ * while its "Enter to select" footer is rendered. When the footer is gone
30626
+ * for INTERACTIVE_PROMPT_LOST_GRACE_MS the prompt is genuinely resolved.
30627
+ */
30628
+ maybeClearResolvedClaudeTuiPrompt() {
30629
+ if (this.cliType !== "claude-cli" || !this.activeInteractivePrompt) return;
30630
+ let screenText = "";
30631
+ try {
30632
+ screenText = this.driver.snapshot();
30633
+ } catch {
30634
+ return;
30635
+ }
30636
+ const stillOnScreen = screenText.includes("Enter to select");
30637
+ if (stillOnScreen) {
30638
+ this.interactivePromptLostAt = null;
30639
+ return;
30640
+ }
30641
+ const lostAt = this.interactivePromptLostAt ?? Date.now();
30642
+ if (this.interactivePromptLostAt === null) this.interactivePromptLostAt = lostAt;
30643
+ if (Date.now() - lostAt < _SpecCliAdapter.INTERACTIVE_PROMPT_LOST_GRACE_MS) return;
30644
+ this.activeInteractivePrompt = null;
30645
+ this.interactivePromptTransport = null;
30646
+ this.interactivePromptLostAt = null;
30647
+ this.statusCallback?.();
30648
+ }
30555
30649
  maybeCaptureClaudeTuiPrompt() {
30556
30650
  if (this.cliType !== "claude-cli" || this.activeInteractivePrompt || this.claudeTuiPromptCaptureInFlight) return;
30557
30651
  const screenText = this.driver.snapshot();
@@ -30565,6 +30659,7 @@ var SpecCliAdapter = class {
30565
30659
  if (!prompt) return;
30566
30660
  this.activeInteractivePrompt = prompt;
30567
30661
  this.interactivePromptTransport = "tui";
30662
+ this.interactivePromptLostAt = null;
30568
30663
  this.statusCallback?.();
30569
30664
  return;
30570
30665
  }
@@ -30601,6 +30696,7 @@ var SpecCliAdapter = class {
30601
30696
  if (!prompt) return;
30602
30697
  this.activeInteractivePrompt = prompt;
30603
30698
  this.interactivePromptTransport = "tui";
30699
+ this.interactivePromptLostAt = null;
30604
30700
  this.statusCallback?.();
30605
30701
  }
30606
30702
  getDebugState() {
@@ -30655,6 +30751,9 @@ var SpecCliAdapter = class {
30655
30751
  // and countdown. This is the canonical "why isn't it transitioning"
30656
30752
  // answer — no screenshots needed.
30657
30753
  fsm: this.driver.getFsmDebug?.() ?? null,
30754
+ // v4 FSM transition snapshot history — the captured pre-transition
30755
+ // evaluation table at each transition (null for v3 specs).
30756
+ fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
30658
30757
  messages,
30659
30758
  committedMessages: messages
30660
30759
  };