@adhdev/daemon-core 0.9.82-rc.137 → 0.9.82-rc.138

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 (39) hide show
  1. package/dist/chat/source-machine.d.ts +166 -0
  2. package/dist/chat/source-resolver.d.ts +104 -0
  3. package/dist/cli-adapters/cli-state-engine.d.ts +15 -0
  4. package/dist/cli-adapters/provider-cli-adapter.d.ts +0 -1
  5. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  6. package/dist/cli-adapters/provider-cli-shared.d.ts +1 -0
  7. package/dist/index.js +922 -328
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +922 -328
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/mesh/contracts.d.ts +164 -0
  12. package/dist/providers/contracts.d.ts +19 -0
  13. package/dist/providers/read-chat-contract.d.ts +29 -0
  14. package/dist/providers/transcript-v2.d.ts +176 -0
  15. package/dist/shared-types.d.ts +7 -0
  16. package/dist/status/snapshot.d.ts +1 -0
  17. package/dist/types.d.ts +5 -0
  18. package/package.json +1 -1
  19. package/src/chat/source-machine.ts +534 -0
  20. package/src/chat/source-resolver.ts +0 -0
  21. package/src/chat/subscription-updates.ts +9 -0
  22. package/src/cli-adapters/cli-state-engine.ts +103 -6
  23. package/src/cli-adapters/provider-cli-adapter.ts +51 -5
  24. package/src/cli-adapters/provider-cli-parse.ts +3 -0
  25. package/src/cli-adapters/provider-cli-shared.ts +13 -1
  26. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  27. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  28. package/src/commands/chat-commands.ts +712 -381
  29. package/src/commands/router.ts +14 -2
  30. package/src/config/chat-history.ts +36 -13
  31. package/src/mesh/contracts.ts +329 -0
  32. package/src/providers/contracts.ts +19 -0
  33. package/src/providers/provider-loader.ts +21 -7
  34. package/src/providers/provider-schema.ts +10 -0
  35. package/src/providers/read-chat-contract.ts +74 -14
  36. package/src/providers/transcript-v2.ts +567 -0
  37. package/src/shared-types.ts +7 -0
  38. package/src/status/snapshot.ts +35 -11
  39. package/src/types.ts +5 -0
@@ -93,6 +93,7 @@ const MAX_FINISH_RETRIES = 2;
93
93
  const FINISH_RETRY_DELAY_MS = 300;
94
94
  const MAX_TRACE_ENTRIES = 250;
95
95
  const APPROVAL_EXIT_TIMEOUT_MS = 60_000;
96
+ const IDLE_CONFIRMATION_GRACE_MS = 2_000;
96
97
 
97
98
  // ─── Engine ────────────────────────────────────────────────────────────────
98
99
 
@@ -133,6 +134,21 @@ export class CliStateEngine {
133
134
  // ── Idle candidate ───────────────────────────────
134
135
  private idleFinishCandidate: IdleFinishCandidate | null = null;
135
136
 
137
+ // ── Idle confirmation grace ──────────────────────
138
+ /**
139
+ * `finishResponse` produces the `generating → idle` transition that
140
+ * coordinators interpret as "task complete". Some providers (antigravity-
141
+ * cli observed in the wild) briefly paint a screen that looks like an
142
+ * idle prompt between tool result frames while still actively running,
143
+ * which fired `response_finished` and broke completion semantics.
144
+ * We defer the actual idle transition by IDLE_CONFIRMATION_GRACE_MS and
145
+ * cancel it if the scripted detection re-detects generating during that
146
+ * window — a true completion stays idle for many seconds, so a 2-second
147
+ * grace is sufficient to filter the paint blip.
148
+ */
149
+ private pendingIdleFinishTimer: NodeJS.Timeout | null = null;
150
+ private pendingIdleFinishAt = 0;
151
+
136
152
  // ── Status history (debug) ───────────────────────
137
153
  private statusHistory: { status: string; at: number; trigger?: string }[] = [];
138
154
  private traceEntries: CliTraceEntry[] = [];
@@ -333,6 +349,7 @@ export class CliStateEngine {
333
349
  if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
334
350
  if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
335
351
  if (this.providerErrorRetryTimer) { clearTimeout(this.providerErrorRetryTimer); this.providerErrorRetryTimer = null; }
352
+ if (this.pendingIdleFinishTimer) { clearTimeout(this.pendingIdleFinishTimer); this.pendingIdleFinishTimer = null; this.pendingIdleFinishAt = 0; }
336
353
  this.providerErrorRetryKey = '';
337
354
  }
338
355
 
@@ -501,8 +518,19 @@ export class CliStateEngine {
501
518
  `[${this.provider.type}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} status=${String(status || '')} parsedStatus=${String(parsedStatus || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify((lastParsedAssistant?.content || '').slice(0, 120)).slice(0, 160)} responseBuffer=${JSON.stringify((snap.responseBuffer || '').slice(0, 160)).slice(0, 220)}`
502
519
  );
503
520
 
504
- const shouldHoldGenerating = status === 'idle' && this.isWaitingForResponse && !modal
505
- && recentInteractiveActivity && !(parsedStatus === 'idle' && !!lastParsedAssistant);
521
+ // recent_activity_hold protects an in-flight user turn from a false
522
+ // idle blip. It must NOT fire during startup — when the adapter has
523
+ // no currentTurnScope, there is no user turn to protect; the recent
524
+ // activity is just the CLI painting its welcome screen. Firing here
525
+ // produced the startup status flip the user reported
526
+ // (generating → idle → generating → idle within the first few seconds
527
+ // of claude-cli launch).
528
+ const shouldHoldGenerating = status === 'idle'
529
+ && this.isWaitingForResponse
530
+ && !!this.currentTurnScope
531
+ && !modal
532
+ && recentInteractiveActivity
533
+ && !(parsedStatus === 'idle' && !!lastParsedAssistant);
506
534
 
507
535
  if (shouldHoldGenerating) { this.applyHoldGenerating(ctx); return; }
508
536
  if (status === 'error') {
@@ -588,20 +616,51 @@ export class CliStateEngine {
588
616
  if (!inCooldown) {
589
617
  if (!modal) {
590
618
  LOG.warn('CLI', `[${this.provider.type}] detectStatus=waiting_approval but parseApproval returned null; ignoring`);
619
+ // (fix) If we previously surfaced waiting_approval but the
620
+ // modal extraction is now failing, do NOT keep the status
621
+ // pinned to waiting_approval forever — the dashboard would
622
+ // show a "waiting" badge with no buttons (activeModal=null)
623
+ // and the user perceives the agent as stuck. Drop the modal
624
+ // and fall back to generating so the rest of the run can
625
+ // settle normally; a future evaluate with a real modal will
626
+ // re-enter waiting_approval cleanly.
627
+ if (this.currentStatus === 'waiting_approval') {
628
+ this.activeModal = null;
629
+ this.setStatus('generating', 'approval_lost_modal');
630
+ this.callbacks.onStatusChange();
631
+ }
591
632
  return;
592
633
  }
593
634
  this.isWaitingForResponse = true;
594
635
  this.setStatus('waiting_approval', 'script_detect');
595
- this.activeModal = modal;
636
+ // (fix) Don't overwrite an already-captured modal with a fresh
637
+ // re-parse on every evaluate — Claude TUI redraws option labels
638
+ // partially between paints (e.g. "Yes, and don't ask again for ..."
639
+ // is wider than the row and ships with a different trailing
640
+ // string each paint), which made the dashboard flap the modal
641
+ // signature continuously. Keep the first stable modal whose
642
+ // button count matches the latest parse; only swap when the
643
+ // shape clearly changed (different number of buttons → different
644
+ // approval).
645
+ const prev = this.activeModal;
646
+ const prevBtnCount = Array.isArray(prev?.buttons) ? prev!.buttons.length : 0;
647
+ const nextBtnCount = Array.isArray(modal.buttons) ? modal.buttons.length : 0;
648
+ if (!prev || prevBtnCount !== nextBtnCount) {
649
+ this.activeModal = modal;
650
+ this.callbacks.onStatusChange();
651
+ }
596
652
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
597
653
  this.armApprovalExitTimeout();
598
- this.callbacks.onStatusChange();
599
654
  }
600
655
  }
601
656
 
602
657
  private applyGenerating(ctx: SettledEvalContext): void {
603
658
  const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
604
659
  this.clearIdleFinishCandidate('generating');
660
+ // Cancel any pending grace-window idle transition. We have fresh
661
+ // evidence the provider is still generating; the previous
662
+ // finishResponse() was a paint blip, not a real completion.
663
+ this.cancelPendingIdleFinish('generating_signal_returned');
605
664
  const snap = this.transport.getSnapshot();
606
665
  const effectiveScreenText = snap.screenText || snap.accumulatedBuffer;
607
666
  const noActiveTurn = !this.currentTurnScope;
@@ -762,11 +821,49 @@ export class CliStateEngine {
762
821
  }
763
822
  this.resetActiveTurnState();
764
823
  this.callbacks.onTurnCompleted();
765
- this.setStatus('idle', 'response_finished');
766
- this.callbacks.onStatusChange();
824
+ // Defer the actual `generating → idle` transition by a short grace
825
+ // window. If applyGenerating fires again before the grace expires —
826
+ // antigravity's tool-result paint blips do this — cancelPendingIdle
827
+ // Finish() drops the pending transition and we stay generating.
828
+ this.scheduleIdleFinish('response_finished');
767
829
  this.transport.flushOutboundQueue();
768
830
  }
769
831
 
832
+ private scheduleIdleFinish(reason: string): void {
833
+ // If we are already deferring, replace the schedule so the most
834
+ // recent finishResponse "wins". Reasons accumulate via the trigger.
835
+ if (this.pendingIdleFinishTimer) clearTimeout(this.pendingIdleFinishTimer);
836
+ this.pendingIdleFinishAt = Date.now() + IDLE_CONFIRMATION_GRACE_MS;
837
+ this.pendingIdleFinishTimer = setTimeout(() => {
838
+ this.pendingIdleFinishTimer = null;
839
+ this.pendingIdleFinishAt = 0;
840
+ // If a new user turn started during the grace window, we owe
841
+ // generating semantics to that turn — do not retroactively idle.
842
+ if (this.isWaitingForResponse) return;
843
+ // The timer firing without a cancelPendingIdleFinish call means
844
+ // no fresh applyGenerating happened during the grace window;
845
+ // the previous fake-blip is over. Commit the idle.
846
+ //
847
+ // Note: the previous "currentStatus === 'generating' → return"
848
+ // guard caused stuck-generating sessions — finishResponse leaves
849
+ // currentStatus='generating' on purpose (so the dashboard keeps
850
+ // the spinner during the 2s grace), and then the timer fired
851
+ // without cancellation but refused to commit because the very
852
+ // status we are about to transition out of was still set.
853
+ // Re-checking currentStatus there meant idle was unreachable.
854
+ this.setStatus('idle', reason);
855
+ this.callbacks.onStatusChange();
856
+ }, IDLE_CONFIRMATION_GRACE_MS);
857
+ }
858
+
859
+ private cancelPendingIdleFinish(reason: string): void {
860
+ if (!this.pendingIdleFinishTimer) return;
861
+ clearTimeout(this.pendingIdleFinishTimer);
862
+ this.pendingIdleFinishTimer = null;
863
+ this.pendingIdleFinishAt = 0;
864
+ this.recordTrace('idle_finish_cancelled', { trigger: reason });
865
+ }
866
+
770
867
  // ─── Helpers ────────────────────────────────────────────────────────────
771
868
 
772
869
  private armApprovalExitTimeout(): void {
@@ -170,9 +170,10 @@ export class ProviderCliAdapter implements CliAdapter {
170
170
  // Resize redraw suppression
171
171
  private resizeSuppressUntil: number = 0;
172
172
 
173
- // Native transcript anchor when >0, native history was confirmed for this session.
174
- // Prevents freshEnough flips caused by PTY buffer activity after the first successful native read.
175
- nativeHistoryAnchoredAt: number = 0;
173
+ // (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
174
+ // ChatSourceMachine holds the lock by state, not by a mutable field on
175
+ // the adapter. Removed entirely; no callers remain after the readChat
176
+ // ladder was replaced.
176
177
 
177
178
  // ─── Script runner (parsing isolated here, adapter stays as transport) ───
178
179
  private readonly runner: CliScriptRunner;
@@ -666,6 +667,14 @@ export class ProviderCliAdapter implements CliAdapter {
666
667
  this.engine.lastApprovalResolvedAt = Date.now();
667
668
  }
668
669
  this.engine.activeModal = null;
670
+ // Clear the in-flight turn flag at the same time we declare
671
+ // startup-idle. Otherwise the next settled evaluation sees
672
+ // isWaitingForResponse=true + recent CLI welcome-screen paints
673
+ // and flips us right back to generating via the hold path
674
+ // (the "startup → generating → idle → generating → idle"
675
+ // flicker the user observed on claude-cli launch).
676
+ this.engine.isWaitingForResponse = false;
677
+ this.engine.currentTurnScope = null;
669
678
  this.engine.setStatus('idle', `startup_ready:${trigger}`);
670
679
  }
671
680
  LOG.info(
@@ -760,6 +769,7 @@ export class ProviderCliAdapter implements CliAdapter {
760
769
  isWaitingForResponse: this.engine.isWaitingForResponse,
761
770
  scope: this.engine.currentTurnScope,
762
771
  runtimeSettings: this.runtimeSettings,
772
+ spawnAt: this.spawnAt,
763
773
  });
764
774
  const session = this.runner.parseSession({
765
775
  ...input,
@@ -801,7 +811,7 @@ export class ProviderCliAdapter implements CliAdapter {
801
811
  const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
802
812
  ? parsed.providerSessionId.trim()
803
813
  : '';
804
- if (providerSessionId) {
814
+ if (providerSessionId && providerSessionId !== this.providerSessionId) {
805
815
  this.providerSessionId = providerSessionId;
806
816
  this.updateRuntimeMeta({ providerSessionId });
807
817
  }
@@ -824,7 +834,42 @@ export class ProviderCliAdapter implements CliAdapter {
824
834
  : null;
825
835
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
826
836
  let effectiveModal = startupModal || this.engine.activeModal;
827
- if (startupDetectedStatus === 'waiting_approval') {
837
+ // (fix) When we have no captured modal yet, take one more live attempt
838
+ // with the current screen text — the engine's settle pass can miss
839
+ // the modal when it happens to fire exactly between writes, and
840
+ // without a modal here the dashboard could never show the buttons.
841
+ // We deliberately do NOT overwrite an existing engine.activeModal so
842
+ // a stable matched modal wins. This runs even outside the startup
843
+ // gate because Claude's approval frames can appear long after launch.
844
+ if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
845
+ const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
846
+ if (liveDetect === 'waiting_approval') {
847
+ const liveModal = this.runParseApproval(this.terminalScreen.getText())
848
+ || this.runParseApproval(this.recentOutputBuffer);
849
+ if (liveModal) {
850
+ effectiveModal = liveModal;
851
+ // Promote so subsequent calls don't re-walk the buffer.
852
+ // Only set if engine hasn't already captured one — keeps
853
+ // the first stable modal as authoritative.
854
+ if (!this.engine.activeModal) this.engine.activeModal = liveModal;
855
+ } else {
856
+ LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: detect=waiting_approval but parseApproval still null (recentLen=${this.recentOutputBuffer.length} screenLen=${this.terminalScreen.getText().length})`);
857
+ }
858
+ } else if (liveDetect && liveDetect !== 'generating' && liveDetect !== 'idle') {
859
+ LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: detect=${liveDetect} (not waiting_approval)`);
860
+ } else if (this.engine.currentStatus === 'waiting_approval' && liveDetect !== 'waiting_approval') {
861
+ LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: engine.status=waiting_approval but live detect=${liveDetect}`);
862
+ }
863
+ } else if (!effectiveModal && this.engine.currentStatus === 'waiting_approval') {
864
+ LOG.warn('CLI', `[${this.cliType}] getStatus skipped live re-extract: allowParse=${allowParse} isWaitingForResponse=${this.engine.isWaitingForResponse}`);
865
+ }
866
+ // Only surface waiting_approval when we ALSO have a concrete modal
867
+ // (message + buttons). detectStatus alone can fire while parseApproval
868
+ // is still null — the engine logs "detectStatus=waiting_approval but
869
+ // parseApproval returned null; ignoring". Without this guard getStatus
870
+ // was shipping a bare waiting_approval with activeModal=null and the
871
+ // user perceived the flow as broken.
872
+ if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
828
873
  effectiveStatus = 'waiting_approval';
829
874
  } else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
830
875
  effectiveStatus = 'idle';
@@ -964,6 +1009,7 @@ export class ProviderCliAdapter implements CliAdapter {
964
1009
  isWaitingForResponse: this.engine.isWaitingForResponse,
965
1010
  scope: this.engine.currentTurnScope,
966
1011
  runtimeSettings: this.runtimeSettings,
1012
+ spawnAt: this.spawnAt,
967
1013
  });
968
1014
  return await Promise.resolve(this.runner.invokeByName(scriptName, {
969
1015
  ...input,
@@ -43,6 +43,7 @@ export function buildCliParseInput(options: {
43
43
  isWaitingForResponse?: boolean;
44
44
  scope?: TurnParseScope | null;
45
45
  runtimeSettings: Record<string, any>;
46
+ spawnAt?: number;
46
47
  }): CliScriptInput {
47
48
  const {
48
49
  accumulatedBuffer,
@@ -57,6 +58,7 @@ export function buildCliParseInput(options: {
57
58
  isWaitingForResponse,
58
59
  scope,
59
60
  runtimeSettings,
61
+ spawnAt,
60
62
  } = options;
61
63
  const buffer = scope
62
64
  ? sliceFromOffset(accumulatedBuffer, scope.bufferStart)
@@ -84,6 +86,7 @@ export function buildCliParseInput(options: {
84
86
  isWaitingForResponse,
85
87
  promptText: scope?.prompt || '',
86
88
  settings: { ...runtimeSettings },
89
+ ...(typeof spawnAt === 'number' && spawnAt > 0 ? { spawnAt } : {}),
87
90
  };
88
91
  }
89
92
 
@@ -119,6 +119,7 @@ export interface CliScriptInput {
119
119
  promptText?: string;
120
120
  settings?: Record<string, any>;
121
121
  args?: Record<string, any>;
122
+ spawnAt?: number;
122
123
  }
123
124
 
124
125
  export interface CliStatusInput {
@@ -362,7 +363,18 @@ export class TerminalTranscriptAccumulator {
362
363
  this.ensureRow();
363
364
  if (final === 'A') this.row = Math.max(0, this.row - count);
364
365
  else if (final === 'B') this.row += count;
365
- else if (final === 'C') this.col += count;
366
+ else if (final === 'C') {
367
+ // (fix) Cursor-forward must materialize spaces in the cells it
368
+ // skips, otherwise rendered transcripts collapse "Do you" written
369
+ // as "Do\x1b[1Cyou" into "Doyou". That mis-rendering broke
370
+ // Claude Code's approval-prompt and prompt-line detection
371
+ // (parseApproval saw "Doyouwanttoproceed?" and returned null).
372
+ const line = this.lines[this.row];
373
+ for (let c = this.col; c < this.col + count; c += 1) {
374
+ if (line[c] === undefined) line[c] = ' ';
375
+ }
376
+ this.col += count;
377
+ }
366
378
  else if (final === 'D') this.col = Math.max(0, this.col - count);
367
379
  else if (final === 'G') this.col = Math.max(0, count - 1);
368
380
  else if (final === 'H' || final === 'f') {
@@ -121,7 +121,23 @@ export class GhosttyVtTerminalBackend implements TerminalViewportBackend {
121
121
  }
122
122
 
123
123
  getText(): string {
124
- return this.terminal.formatPlainText({ trim: true }) || '';
124
+ // (fix) ghostty's `trim:true` mode strips trailing whitespace per row
125
+ // AND collapses cells that were touched only by cursor-forward (ESC[<n>C)
126
+ // without a printable glyph. Many TUIs (Claude Code most prominently)
127
+ // render inter-word spaces via CUF rather than literal spaces, so
128
+ // trim:true would smash "Do you want to proceed?" into
129
+ // "Doyouwanttoproceed?" and break every downstream regex
130
+ // (approval detection, prompt-line discovery, etc.). Keep the per-row
131
+ // padding, then trim each row's trailing whitespace ourselves so the
132
+ // serialized text matches what the user sees in the terminal.
133
+ const raw = this.terminal.formatPlainText({ trim: false }) || '';
134
+ if (!raw) return '';
135
+ const lines = raw.split('\n').map((row) => row.replace(/\s+$/, ''));
136
+ let first = 0;
137
+ let last = lines.length;
138
+ while (first < last && !lines[first]) first += 1;
139
+ while (last > first && !lines[last - 1]) last -= 1;
140
+ return lines.slice(first, last).join('\n');
125
141
  }
126
142
 
127
143
  getCursorPosition(): { col: number; row: number } {
@@ -64,7 +64,14 @@ export class XtermTerminalBackend implements TerminalViewportBackend {
64
64
 
65
65
  for (let i = start; i < end; i++) {
66
66
  const line = buffer.getLine(i);
67
- lines.push(line ? line.translateToString(true) : '');
67
+ // (fix) translateToString(true) strips trailing whitespace per row
68
+ // AND collapses cells touched only by cursor-forward (ESC[<n>C),
69
+ // so Claude Code's "Do you want to proceed?" arrives as
70
+ // "Doyouwanttoproceed?" — every downstream approval/prompt regex
71
+ // misses. Use false to preserve inter-word padding; we trim each
72
+ // row's trailing whitespace ourselves below.
73
+ const raw = line ? line.translateToString(false) : '';
74
+ lines.push(raw.replace(/\s+$/, ''));
68
75
  }
69
76
 
70
77
  let first = 0;