@adhdev/daemon-core 0.7.45 → 0.8.0

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 (54) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +34 -0
  2. package/dist/cli-adapters/pty-transport.d.ts +1 -0
  3. package/dist/cli-adapters/session-host-transport.d.ts +1 -0
  4. package/dist/commands/cli-manager.d.ts +11 -2
  5. package/dist/config/chat-history.d.ts +32 -2
  6. package/dist/config/config.d.ts +5 -1
  7. package/dist/config/recent-activity.d.ts +3 -1
  8. package/dist/config/saved-sessions.d.ts +22 -0
  9. package/dist/daemon/dev-auto-implement.d.ts +18 -2
  10. package/dist/daemon/dev-cli-debug.d.ts +82 -0
  11. package/dist/daemon/dev-server.d.ts +7 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +6122 -4038
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +6114 -4032
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/providers/cli-provider-instance.d.ts +29 -1
  18. package/dist/providers/contracts.d.ts +11 -0
  19. package/dist/providers/provider-instance.d.ts +1 -0
  20. package/dist/shared-types.d.ts +2 -0
  21. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +12 -1
  22. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +12 -1
  23. package/node_modules/@adhdev/session-host-core/dist/index.js +9 -0
  24. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  25. package/node_modules/@adhdev/session-host-core/dist/index.mjs +9 -0
  26. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  27. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  28. package/package.json +1 -1
  29. package/src/boot/daemon-lifecycle.ts +19 -15
  30. package/src/cli-adapters/provider-cli-adapter.ts +424 -7
  31. package/src/cli-adapters/pty-transport.ts +1 -0
  32. package/src/cli-adapters/session-host-transport.ts +32 -1
  33. package/src/commands/chat-commands.ts +36 -8
  34. package/src/commands/cli-manager.ts +259 -22
  35. package/src/commands/router.ts +52 -1
  36. package/src/config/chat-history.ts +197 -10
  37. package/src/config/config.d.ts +4 -0
  38. package/src/config/config.ts +8 -2
  39. package/src/config/recent-activity.ts +13 -2
  40. package/src/config/saved-sessions.ts +73 -0
  41. package/src/daemon/dev-auto-implement.ts +394 -43
  42. package/src/daemon/dev-cli-debug.ts +839 -0
  43. package/src/daemon/dev-server.ts +51 -5
  44. package/src/index.ts +2 -0
  45. package/src/providers/cli-provider-instance.ts +283 -4
  46. package/src/providers/contracts.ts +11 -0
  47. package/src/providers/provider-instance.d.ts +1 -0
  48. package/src/providers/provider-instance.ts +1 -0
  49. package/src/providers/provider-loader.ts +39 -0
  50. package/src/session-host/runtime-support.ts +1 -0
  51. package/src/shared-types.d.ts +2 -0
  52. package/src/shared-types.ts +2 -0
  53. package/src/status/builders.ts +1 -0
  54. package/src/status/snapshot.ts +1 -0
@@ -100,6 +100,24 @@ interface TurnParseScope {
100
100
  rawBufferStart: number;
101
101
  }
102
102
 
103
+ interface IdleFinishCandidate {
104
+ armedAt: number;
105
+ lastOutputAt: number;
106
+ lastScreenChangeAt: number;
107
+ responseEpoch: number;
108
+ assistantLength: number;
109
+ }
110
+
111
+ export interface CliTraceEntry {
112
+ id: number;
113
+ at: number;
114
+ type: string;
115
+ status: CliSessionStatus['status'];
116
+ isWaitingForResponse: boolean;
117
+ activeModal: { message: string; buttons: string[] } | null;
118
+ payload: Record<string, any>;
119
+ }
120
+
103
121
  export interface CliProviderModule {
104
122
  type: string;
105
123
  name: string;
@@ -140,13 +158,15 @@ export interface CliProviderModule {
140
158
  function stripAnsi(str: string): string {
141
159
  // eslint-disable-next-line no-control-regex
142
160
  return str
161
+ // OSC sequences (title bar etc) — strip before generic ESC removal so payload cannot leak.
162
+ .replace(/\x1B\][^\x07]*\x07/g, '')
163
+ .replace(/\x1B\][\s\S]*?\x1B\\/g, '')
164
+ // DCS / APC / PM / SOS control strings terminated by ST or BEL.
165
+ .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
143
166
  // Cursor movement sequences → space (prevents word concatenation)
144
167
  .replace(/\x1B\[\d*[A-HJKSTfG]/g, ' ')
145
168
  // SGR and other CSI sequences → remove
146
169
  .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
147
- // OSC sequences (title bar etc)
148
- .replace(/\x1B\][^\x07]*\x07/g, '')
149
- .replace(/\x1B\][^\x1B]*\x1B\\/g, '')
150
170
  // Collapse multiple spaces
151
171
  .replace(/ +/g, ' ');
152
172
  }
@@ -160,6 +180,11 @@ function stripTerminalNoise(str: string): string {
160
180
  .replace(/(^|[\s([])(?:\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
161
181
  .replace(/(^|[\s([])(?:\[\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
162
182
  .replace(/(^|[\s([])(?:\d{1,4};\?)(?=$|[\s)\]])/g, '$1')
183
+ // Drop common leftover DCS/OSC payload fragments when a control string was split across PTY chunks.
184
+ .replace(/(^|[\s([])(?:\d+\$r[0-9;\" ]*[A-Za-z]?)(?=$|[\s)\]])/g, '$1')
185
+ .replace(/(^|[\s([])(?:>\|[A-Za-z0-9_.:-]+(?:\([^)]*\))?)(?=$|[\s)\]])/g, '$1')
186
+ .replace(/(^|[\s([])(?:[A-Z]\d(?:\s+[A-Z]\d)+)(?=$|[\s)\]])/g, '$1')
187
+ .replace(/(^|[\s([])(?:\d+;[^\s)\]]+)(?=$|[\s)\]])/g, '$1')
163
188
  .replace(/\r+/g, '\n')
164
189
  .replace(/[ \t]+\n/g, '\n')
165
190
  .replace(/\n{3,}/g, '\n\n')
@@ -326,6 +351,12 @@ function promptLikelyVisible(screenText: string, promptSnippet: string): boolean
326
351
  return matched >= required;
327
352
  }
328
353
 
354
+ function normalizeScreenSnapshot(text: string): string {
355
+ return sanitizeTerminalText(String(text || ''))
356
+ .replace(/\s+/g, ' ')
357
+ .trim();
358
+ }
359
+
329
360
  /**
330
361
  * Normalize provider.json for auto-implement approval detection.
331
362
  * Kept for backward compat with dev-server auto-impl pipeline only.
@@ -395,6 +426,10 @@ export class ProviderCliAdapter implements CliAdapter {
395
426
  private ptyOutputBuffer = '';
396
427
  private ptyOutputFlushTimer: NodeJS.Timeout | null = null;
397
428
  private pendingTerminalQueryTail = '';
429
+ private lastOutputAt = 0;
430
+ private lastNonEmptyOutputAt = 0;
431
+ private lastScreenChangeAt = 0;
432
+ private lastScreenSnapshot = '';
398
433
 
399
434
  // Server log forwarding
400
435
  private serverConn: any = null;
@@ -419,6 +454,7 @@ export class ProviderCliAdapter implements CliAdapter {
419
454
  private submitRetryTimer: NodeJS.Timeout | null = null;
420
455
  private submitRetryUsed = false;
421
456
  private submitRetryPromptSnippet = '';
457
+ private idleFinishCandidate: IdleFinishCandidate | null = null;
422
458
 
423
459
  // Resize redraw suppression
424
460
  private resizeSuppressUntil: number = 0;
@@ -433,10 +469,16 @@ export class ProviderCliAdapter implements CliAdapter {
433
469
  /** Full accumulated raw PTY output (with ANSI) */
434
470
  private accumulatedRawBuffer: string = '';
435
471
  /** Current visible terminal screen snapshot */
436
- private terminalScreen = new TerminalScreen(30, 100);
472
+ private terminalScreen = new TerminalScreen(24, 80);
437
473
  /** Max accumulated buffer size (last 50KB) */
438
474
  private static readonly MAX_ACCUMULATED_BUFFER = 50000;
439
475
  private currentTurnScope: TurnParseScope | null = null;
476
+ private traceEntries: CliTraceEntry[] = [];
477
+ private traceSeq = 0;
478
+ private traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
479
+ private static readonly MAX_TRACE_ENTRIES = 250;
480
+ private readonly providerResolutionMeta: Record<string, any>;
481
+ private static readonly IDLE_FINISH_CONFIRM_MS = 900;
440
482
 
441
483
  private syncMessageViews(): void {
442
484
  this.messages = [...this.committedMessages];
@@ -486,9 +528,103 @@ export class ProviderCliAdapter implements CliAdapter {
486
528
  this.currentStatus = status;
487
529
  this.statusHistory.push({ status, at: Date.now(), trigger });
488
530
  if (this.statusHistory.length > 50) this.statusHistory.shift();
531
+ this.recordTrace('status', {
532
+ previousStatus: prev,
533
+ trigger: trigger || null,
534
+ });
489
535
  LOG.info('CLI', `[${this.cliType}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
490
536
  }
491
537
 
538
+ private clearIdleFinishCandidate(reason: string): void {
539
+ if (!this.idleFinishCandidate) return;
540
+ this.recordTrace('idle_candidate_reset', {
541
+ reason,
542
+ candidate: this.idleFinishCandidate,
543
+ });
544
+ this.idleFinishCandidate = null;
545
+ }
546
+
547
+ private armIdleFinishCandidate(assistantLength: number): void {
548
+ const now = Date.now();
549
+ this.idleFinishCandidate = {
550
+ armedAt: now,
551
+ lastOutputAt: this.lastOutputAt,
552
+ lastScreenChangeAt: this.lastScreenChangeAt,
553
+ responseEpoch: this.responseEpoch,
554
+ assistantLength,
555
+ };
556
+ this.recordTrace('idle_candidate_armed', {
557
+ confirmMs: ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS,
558
+ candidate: this.idleFinishCandidate,
559
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
560
+ });
561
+ if (this.settleTimer) clearTimeout(this.settleTimer);
562
+ this.settleTimer = setTimeout(() => {
563
+ this.settleTimer = null;
564
+ this.settledBuffer = this.recentOutputBuffer;
565
+ this.evaluateSettled();
566
+ }, ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS);
567
+ }
568
+
569
+ private summarizeTraceText(text: string, max = 800): string {
570
+ const value = sanitizeTerminalText(String(text || ''));
571
+ if (value.length <= max) return value;
572
+ return `…${value.slice(-max)}`;
573
+ }
574
+
575
+ private summarizeTraceMessages(messages: CliChatMessage[], limit = 3): { role: string; content: string; timestamp?: number }[] {
576
+ return messages.slice(-limit).map((message) => ({
577
+ role: message.role,
578
+ content: this.summarizeTraceText(message.content, 240),
579
+ timestamp: message.timestamp,
580
+ }));
581
+ }
582
+
583
+ private buildTraceParseSnapshot(scope?: TurnParseScope | null, partialResponse = ''): Record<string, any> {
584
+ const scopedBuffer = scope
585
+ ? (this.sliceFromOffset(this.accumulatedBuffer, scope.bufferStart) || this.accumulatedBuffer)
586
+ : this.accumulatedBuffer;
587
+ const scopedRawBuffer = scope
588
+ ? (this.sliceFromOffset(this.accumulatedRawBuffer, scope.rawBufferStart) || this.accumulatedRawBuffer)
589
+ : this.accumulatedRawBuffer;
590
+ return {
591
+ currentTurnScope: scope || null,
592
+ responseBuffer: this.summarizeTraceText(this.responseBuffer, 1200),
593
+ partialResponse: this.summarizeTraceText(partialResponse || this.responseBuffer, 1200),
594
+ turnBuffer: this.summarizeTraceText(scopedBuffer, 1600),
595
+ turnRawPreview: this.summarizeTraceText(scopedRawBuffer, 1600),
596
+ turnSanitizedRawPreview: this.summarizeTraceText(sanitizeTerminalText(scopedRawBuffer), 1600),
597
+ };
598
+ }
599
+
600
+ private recordTrace(type: string, payload: Record<string, any> = {}): void {
601
+ const entry: CliTraceEntry = {
602
+ id: ++this.traceSeq,
603
+ at: Date.now(),
604
+ type,
605
+ status: this.currentStatus,
606
+ isWaitingForResponse: this.isWaitingForResponse,
607
+ activeModal: this.activeModal
608
+ ? { message: this.activeModal.message, buttons: [...this.activeModal.buttons] }
609
+ : null,
610
+ payload,
611
+ };
612
+ this.traceEntries.push(entry);
613
+ if (this.traceEntries.length > ProviderCliAdapter.MAX_TRACE_ENTRIES) {
614
+ this.traceEntries.splice(0, this.traceEntries.length - ProviderCliAdapter.MAX_TRACE_ENTRIES);
615
+ }
616
+ }
617
+
618
+ private resetTraceSession(): void {
619
+ this.traceEntries = [];
620
+ this.traceSeq = 0;
621
+ this.traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
622
+ this.recordTrace('session_start', {
623
+ providerType: this.cliType,
624
+ workingDir: this.workingDir,
625
+ });
626
+ }
627
+
492
628
  // Resolved timeouts
493
629
  private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
494
630
 
@@ -532,12 +668,27 @@ export class ProviderCliAdapter implements CliAdapter {
532
668
  ? (provider as any).sendKey
533
669
  : '\r';
534
670
  this.submitStrategy = (provider as any).submitStrategy === 'immediate' ? 'immediate' : 'wait_for_echo';
671
+ this.providerResolutionMeta = {
672
+ type: provider.type,
673
+ name: provider.name,
674
+ resolvedVersion: (provider as any)._resolvedVersion || null,
675
+ resolvedOs: (provider as any)._resolvedOs || null,
676
+ providerDir: (provider as any)._resolvedProviderDir || null,
677
+ scriptDir: (provider as any)._resolvedScriptDir || null,
678
+ scriptsPath: (provider as any)._resolvedScriptsPath || null,
679
+ scriptsSource: (provider as any)._resolvedScriptsSource || null,
680
+ versionWarning: (provider as any)._versionWarning || null,
681
+ };
535
682
 
536
683
  // Scripts are required — loaded by ProviderLoader via compatibility array
537
684
  this.cliScripts = (provider as any).scripts || {};
538
685
  const scriptNames = Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function');
539
686
  if (scriptNames.length > 0) {
540
687
  LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
688
+ LOG.info(
689
+ 'CLI',
690
+ `[${this.cliType}] Provider resolution: providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'} source=${this.providerResolutionMeta.scriptsSource || '-'} version=${this.providerResolutionMeta.resolvedVersion || '-'}`
691
+ );
541
692
  } else {
542
693
  LOG.warn('CLI', `[${this.cliType}] ⚠ No CLI scripts loaded! Provider needs scripts/{version}/scripts.js`);
543
694
  }
@@ -588,6 +739,7 @@ export class ProviderCliAdapter implements CliAdapter {
588
739
  const allArgs = [...spawnConfig.args, ...this.extraArgs];
589
740
 
590
741
  LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
742
+ this.resetTraceSession();
591
743
 
592
744
  let shellCmd: string;
593
745
  let shellArgs: string[];
@@ -625,6 +777,14 @@ export class ProviderCliAdapter implements CliAdapter {
625
777
  cwd: this.workingDir,
626
778
  env: buildCliSpawnEnv(process.env, spawnConfig.env),
627
779
  };
780
+ this.recordTrace('spawn', {
781
+ shellCommand: shellCmd,
782
+ shellArgs,
783
+ cwd: ptyOpts.cwd,
784
+ cols: ptyOpts.cols,
785
+ rows: ptyOpts.rows,
786
+ providerResolution: this.providerResolutionMeta,
787
+ });
628
788
 
629
789
  try {
630
790
  this.ptyProcess = this.transportFactory.spawn(shellCmd, shellArgs, ptyOpts);
@@ -673,6 +833,7 @@ export class ProviderCliAdapter implements CliAdapter {
673
833
  this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
674
834
  LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
675
835
  this.flushPendingOutputParse();
836
+ this.recordTrace('exit', { exitCode });
676
837
  this.ptyProcess = null;
677
838
  this.setStatus('stopped', 'pty_exit');
678
839
  this.ready = false;
@@ -689,6 +850,9 @@ export class ProviderCliAdapter implements CliAdapter {
689
850
  this.currentTurnScope = null;
690
851
  this.ready = false;
691
852
  await this.ptyProcess.ready;
853
+ this.recordTrace('ready', {
854
+ runtimeMeta: this.getRuntimeMetadata(),
855
+ });
692
856
  this.setStatus('idle', 'pty_ready');
693
857
  this.onStatusChange?.();
694
858
  }
@@ -698,6 +862,24 @@ export class ProviderCliAdapter implements CliAdapter {
698
862
  private handleOutput(rawData: string): void {
699
863
  this.terminalScreen.write(rawData);
700
864
  const cleanData = sanitizeTerminalText(rawData);
865
+ const now = Date.now();
866
+ const normalizedScreenSnapshot = normalizeScreenSnapshot(this.terminalScreen.getText());
867
+ this.lastOutputAt = now;
868
+ if (cleanData.trim()) this.lastNonEmptyOutputAt = now;
869
+ if (normalizedScreenSnapshot !== this.lastScreenSnapshot) {
870
+ this.lastScreenSnapshot = normalizedScreenSnapshot;
871
+ this.lastScreenChangeAt = now;
872
+ }
873
+ if (this.idleFinishCandidate && (rawData.length > 0 || cleanData.length > 0)) {
874
+ this.clearIdleFinishCandidate('new_output');
875
+ }
876
+ this.recordTrace('output', {
877
+ rawLength: rawData.length,
878
+ cleanLength: cleanData.length,
879
+ rawPreview: this.summarizeTraceText(rawData, 300),
880
+ cleanPreview: this.summarizeTraceText(cleanData, 300),
881
+ screenText: this.summarizeTraceText(this.terminalScreen.getText(), 1200),
882
+ });
701
883
 
702
884
  if (this.isWaitingForResponse && cleanData) {
703
885
  this.responseBuffer = (this.responseBuffer + cleanData).slice(-8000);
@@ -722,15 +904,20 @@ export class ProviderCliAdapter implements CliAdapter {
722
904
  this.startupBuffer += cleanData;
723
905
  const elapsed = Date.now() - this.spawnAt;
724
906
  const scriptStatus = this.runDetectStatus(this.startupBuffer);
725
- const isReady = scriptStatus === 'idle'
726
- || scriptStatus === 'waiting_approval'
907
+ const screenText = this.terminalScreen.getText() || '';
908
+ const hasInteractivePrompt = this.looksLikeVisibleIdlePrompt(screenText);
909
+ const startupStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
910
+ const isReady = ((scriptStatus === 'idle' || scriptStatus === 'waiting_approval') && hasInteractivePrompt && startupStableMs >= 700)
727
911
  || elapsed > 8000
728
912
  || this.startupBuffer.length > 12000;
729
913
 
730
914
  if (isReady) {
731
915
  this.startupParseGate = false;
732
916
  this.ready = true;
733
- LOG.info('CLI', `[${this.cliType}] Startup ready (${elapsed}ms, scriptStatus=${scriptStatus})`);
917
+ LOG.info(
918
+ 'CLI',
919
+ `[${this.cliType}] Startup ready (${elapsed}ms, scriptStatus=${scriptStatus}, prompt=${hasInteractivePrompt}, stableMs=${startupStableMs}) providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
920
+ );
734
921
  this.onStatusChange?.();
735
922
  }
736
923
  // No early return — status detection runs from the start
@@ -789,6 +976,50 @@ export class ProviderCliAdapter implements CliAdapter {
789
976
  || /for\s*shortcuts/i.test(text);
790
977
  }
791
978
 
979
+ private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
980
+ const startedAt = Date.now();
981
+ let loggedWait = false;
982
+
983
+ while (Date.now() - startedAt < maxWaitMs) {
984
+ const screenText = this.terminalScreen.getText() || '';
985
+ const hasPrompt = this.looksLikeVisibleIdlePrompt(screenText);
986
+ const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
987
+ const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
988
+ const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
989
+ const startupLikelyActive = /Welcome back|Tips for getting|Recent activity|Claude Code v\d/i.test(screenText);
990
+ const interactiveReady = hasPrompt
991
+ && stableMs >= 700
992
+ && recentlyOutput >= 350
993
+ && status !== 'starting'
994
+ && status !== 'generating';
995
+
996
+ if (interactiveReady) {
997
+ if (loggedWait) {
998
+ LOG.info(
999
+ 'CLI',
1000
+ `[${this.cliType}] Interactive prompt ready after ${Date.now() - startedAt}ms (stableMs=${stableMs}, recentOutputMs=${recentlyOutput}, startup=${startupLikelyActive})`
1001
+ );
1002
+ }
1003
+ return;
1004
+ }
1005
+
1006
+ if (!loggedWait && (Date.now() - startedAt) >= 400) {
1007
+ loggedWait = true;
1008
+ LOG.info(
1009
+ 'CLI',
1010
+ `[${this.cliType}] Waiting for interactive prompt: hasPrompt=${hasPrompt} stableMs=${stableMs} recentOutputMs=${recentlyOutput} status=${status} startup=${startupLikelyActive} screen=${JSON.stringify(this.summarizeTraceText(screenText, 220)).slice(0, 260)}`
1011
+ );
1012
+ }
1013
+ await new Promise(resolve => setTimeout(resolve, 50));
1014
+ }
1015
+
1016
+ const finalScreenText = this.terminalScreen.getText() || '';
1017
+ LOG.warn(
1018
+ 'CLI',
1019
+ `[${this.cliType}] Interactive prompt wait timed out after ${maxWaitMs}ms; proceeding with screen=${JSON.stringify(this.summarizeTraceText(finalScreenText, 240)).slice(0, 280)}`
1020
+ );
1021
+ }
1022
+
792
1023
  private evaluateSettled(): void {
793
1024
  const now = Date.now();
794
1025
  if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
@@ -807,6 +1038,32 @@ export class ProviderCliAdapter implements CliAdapter {
807
1038
  const rawScriptStatus = this.runDetectStatus(tail);
808
1039
  // detectStatus is the sole authority for status. parseApproval only enriches modal info.
809
1040
  const scriptStatus = rawScriptStatus;
1041
+ const parsedTranscript = this.parseCurrentTranscript(
1042
+ this.committedMessages,
1043
+ this.responseBuffer,
1044
+ this.currentTurnScope,
1045
+ );
1046
+ const parsedMessages = Array.isArray(parsedTranscript?.messages)
1047
+ ? this.normalizeParsedMessages(parsedTranscript.messages)
1048
+ : [];
1049
+ const lastParsedAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
1050
+ this.recordTrace('settled', {
1051
+ tail: this.summarizeTraceText(tail, 500),
1052
+ screenText: this.summarizeTraceText(screenText, 1200),
1053
+ detectStatus: scriptStatus,
1054
+ parsedStatus: parsedTranscript?.status || null,
1055
+ parsedMessageCount: parsedMessages.length,
1056
+ parsedLastAssistant: lastParsedAssistant ? this.summarizeTraceText(lastParsedAssistant.content, 280) : '',
1057
+ parsedActiveModal: parsedTranscript?.activeModal ?? null,
1058
+ approval: modal,
1059
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1060
+ });
1061
+ if (this.currentTurnScope && !lastParsedAssistant) {
1062
+ LOG.info(
1063
+ 'CLI',
1064
+ `[${this.cliType}] Settled without assistant: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(this.summarizeTraceText(this.responseBuffer, 220)).slice(0, 260)} screen=${JSON.stringify(this.summarizeTraceText(screenText, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
1065
+ );
1066
+ }
810
1067
  if (!scriptStatus) return;
811
1068
 
812
1069
  const prevStatus = this.currentStatus;
@@ -850,6 +1107,7 @@ export class ProviderCliAdapter implements CliAdapter {
850
1107
  }
851
1108
 
852
1109
  if (scriptStatus === 'waiting_approval') {
1110
+ this.clearIdleFinishCandidate('waiting_approval');
853
1111
  const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
854
1112
  const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
855
1113
  if ((inCooldown || visibleIdlePrompt) && !modal) {
@@ -884,6 +1142,7 @@ export class ProviderCliAdapter implements CliAdapter {
884
1142
  }
885
1143
 
886
1144
  if (scriptStatus === 'generating') {
1145
+ this.clearIdleFinishCandidate('generating');
887
1146
  const effectiveScreenText = screenText || this.accumulatedBuffer;
888
1147
  const noActiveTurn = !this.currentTurnScope;
889
1148
  const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText)
@@ -921,13 +1180,71 @@ export class ProviderCliAdapter implements CliAdapter {
921
1180
  this.lastApprovalResolvedAt = Date.now();
922
1181
  }
923
1182
  if (this.isWaitingForResponse) {
1183
+ const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
1184
+ const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1185
+ const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1186
+ const hasAssistantTurn = !!lastParsedAssistant;
1187
+ const assistantLength = lastParsedAssistant?.content?.length || 0;
1188
+ const idleQuietThresholdMs = Math.max(220, this.timeouts.outputSettle);
1189
+ const idleStableThresholdMs = Math.max(120, Math.min(220, this.timeouts.outputSettle));
1190
+ const idleReady = visibleIdlePrompt
1191
+ && !modal
1192
+ && hasAssistantTurn
1193
+ && quietForMs >= idleQuietThresholdMs
1194
+ && screenStableMs >= idleStableThresholdMs;
1195
+ const candidate = this.idleFinishCandidate;
1196
+ const candidateQuiet = !!candidate
1197
+ && candidate.responseEpoch === this.responseEpoch
1198
+ && candidate.lastOutputAt === this.lastOutputAt
1199
+ && candidate.lastScreenChangeAt === this.lastScreenChangeAt
1200
+ && assistantLength >= candidate.assistantLength
1201
+ && (now - candidate.armedAt) >= ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS;
1202
+ const canFinishImmediately = idleReady && candidateQuiet;
1203
+
1204
+ this.recordTrace('idle_decision', {
1205
+ visibleIdlePrompt,
1206
+ quietForMs,
1207
+ screenStableMs,
1208
+ hasAssistantTurn,
1209
+ assistantLength,
1210
+ hasModal: !!modal,
1211
+ idleQuietThresholdMs,
1212
+ idleStableThresholdMs,
1213
+ idleReady,
1214
+ idleFinishConfirmMs: ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS,
1215
+ idleFinishCandidate: candidate,
1216
+ candidateQuiet,
1217
+ canFinishImmediately,
1218
+ submitPendingUntil: this.submitPendingUntil,
1219
+ responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
1220
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1221
+ });
1222
+
1223
+ if (canFinishImmediately) {
1224
+ this.clearIdleFinishCandidate('finish_response');
1225
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1226
+ this.finishResponse();
1227
+ return;
1228
+ }
1229
+
1230
+ if (idleReady) {
1231
+ if (!candidate) {
1232
+ this.armIdleFinishCandidate(assistantLength);
1233
+ return;
1234
+ }
1235
+ } else {
1236
+ this.clearIdleFinishCandidate('idle_not_ready');
1237
+ }
1238
+
924
1239
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
925
1240
  this.idleTimeout = setTimeout(() => {
926
1241
  if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1242
+ this.clearIdleFinishCandidate('idle_timeout_finish');
927
1243
  this.finishResponse();
928
1244
  }
929
1245
  }, this.timeouts.idleFinish);
930
1246
  } else if (prevStatus !== 'idle') {
1247
+ this.clearIdleFinishCandidate('idle_without_response');
931
1248
  this.setStatus('idle', 'script_detect');
932
1249
  this.onStatusChange?.();
933
1250
  }
@@ -937,6 +1254,10 @@ export class ProviderCliAdapter implements CliAdapter {
937
1254
  private finishResponse(): void {
938
1255
  if (this.submitPendingUntil > Date.now()) return;
939
1256
  if (this.responseSettleIgnoreUntil > Date.now()) return;
1257
+ this.clearIdleFinishCandidate('finish_response_enter');
1258
+ this.recordTrace('finish_response', {
1259
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1260
+ });
940
1261
  this.commitCurrentTranscript();
941
1262
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
942
1263
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
@@ -963,6 +1284,20 @@ export class ProviderCliAdapter implements CliAdapter {
963
1284
  if (parsed && Array.isArray(parsed.messages)) {
964
1285
  this.committedMessages = this.normalizeParsedMessages(parsed.messages);
965
1286
  this.syncMessageViews();
1287
+ const lastAssistant = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
1288
+ this.recordTrace('commit_transcript', {
1289
+ parsedStatus: parsed.status || null,
1290
+ messageCount: this.committedMessages.length,
1291
+ lastAssistant: lastAssistant ? this.summarizeTraceText(lastAssistant.content, 320) : '',
1292
+ messages: this.summarizeTraceMessages(this.committedMessages),
1293
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1294
+ });
1295
+ if (!lastAssistant && this.currentTurnScope) {
1296
+ LOG.warn(
1297
+ 'CLI',
1298
+ `[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(this.summarizeTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
1299
+ );
1300
+ }
966
1301
  }
967
1302
  }
968
1303
 
@@ -1025,6 +1360,7 @@ export class ProviderCliAdapter implements CliAdapter {
1025
1360
  title: parsed.title || this.cliName,
1026
1361
  messages: parsed.messages,
1027
1362
  activeModal: parsed.activeModal ?? this.activeModal,
1363
+ providerSessionId: typeof parsed.providerSessionId === 'string' ? parsed.providerSessionId : undefined,
1028
1364
  };
1029
1365
  }
1030
1366
 
@@ -1093,17 +1429,24 @@ export class ProviderCliAdapter implements CliAdapter {
1093
1429
  }
1094
1430
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1095
1431
  if (this.isWaitingForResponse) return;
1432
+ await this.waitForInteractivePrompt();
1096
1433
 
1097
1434
  this.committedMessages.push({ role: 'user', content: text, timestamp: Date.now() });
1098
1435
  this.syncMessageViews();
1099
1436
  this.isWaitingForResponse = true;
1100
1437
  this.responseBuffer = '';
1438
+ this.clearIdleFinishCandidate('send_message');
1101
1439
  this.currentTurnScope = {
1102
1440
  prompt: text,
1103
1441
  startedAt: Date.now(),
1104
1442
  bufferStart: this.accumulatedBuffer.length,
1105
1443
  rawBufferStart: this.accumulatedRawBuffer.length,
1106
1444
  };
1445
+ this.recordTrace('send_message', {
1446
+ text: this.summarizeTraceText(text, 500),
1447
+ estimatedLines: estimatePromptDisplayLines(text),
1448
+ turnScope: this.currentTurnScope,
1449
+ });
1107
1450
  LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
1108
1451
  this.submitRetryUsed = false;
1109
1452
  this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
@@ -1134,6 +1477,11 @@ export class ProviderCliAdapter implements CliAdapter {
1134
1477
  const submit = () => {
1135
1478
  if (!this.ptyProcess) return;
1136
1479
  this.submitPendingUntil = 0;
1480
+ this.recordTrace('submit_write', {
1481
+ mode: 'submit_key',
1482
+ sendKey: this.sendKey,
1483
+ screenText: this.summarizeTraceText(this.terminalScreen.getText(), 500),
1484
+ });
1137
1485
  this.ptyProcess.write(this.sendKey);
1138
1486
  const retrySubmitIfStuck = (attempt: number) => {
1139
1487
  this.submitRetryTimer = null;
@@ -1145,6 +1493,12 @@ export class ProviderCliAdapter implements CliAdapter {
1145
1493
  if (/Esc to interrupt|Do you want to proceed|This command requires approval|Allow Codex to|Approve and run now|Always approve this session|Running…|Running\.\.\./i.test(screenText)) return;
1146
1494
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1147
1495
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
1496
+ this.recordTrace('submit_write', {
1497
+ mode: 'submit_retry',
1498
+ attempt,
1499
+ sendKey: this.sendKey,
1500
+ screenText: this.summarizeTraceText(screenText, 500),
1501
+ });
1148
1502
  this.ptyProcess.write(this.sendKey);
1149
1503
  if (attempt >= 3) {
1150
1504
  this.submitRetryUsed = true;
@@ -1158,6 +1512,12 @@ export class ProviderCliAdapter implements CliAdapter {
1158
1512
 
1159
1513
  if (this.submitStrategy === 'immediate') {
1160
1514
  this.submitPendingUntil = 0;
1515
+ this.recordTrace('submit_write', {
1516
+ mode: 'immediate',
1517
+ text: this.summarizeTraceText(text, 500),
1518
+ sendKey: this.sendKey,
1519
+ screenText: this.summarizeTraceText(this.terminalScreen.getText(), 500),
1520
+ });
1161
1521
  this.ptyProcess.write(text + this.sendKey);
1162
1522
  this.submitRetryTimer = setTimeout(() => {
1163
1523
  this.submitRetryTimer = null;
@@ -1168,6 +1528,12 @@ export class ProviderCliAdapter implements CliAdapter {
1168
1528
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
1169
1529
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
1170
1530
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1531
+ this.recordTrace('submit_write', {
1532
+ mode: 'immediate_retry',
1533
+ attempt: 1,
1534
+ sendKey: this.sendKey,
1535
+ screenText: this.summarizeTraceText(screenText, 500),
1536
+ });
1171
1537
  this.ptyProcess.write(this.sendKey);
1172
1538
  this.submitRetryUsed = true;
1173
1539
  }, retryDelayMs);
@@ -1179,6 +1545,12 @@ export class ProviderCliAdapter implements CliAdapter {
1179
1545
  this.submitPendingUntil = Date.now() + submitDelayMs;
1180
1546
  }
1181
1547
  this.ptyProcess.write(text);
1548
+ this.recordTrace('submit_write', {
1549
+ mode: 'type_then_submit',
1550
+ text: this.summarizeTraceText(text, 500),
1551
+ sendKey: this.sendKey,
1552
+ screenText: this.summarizeTraceText(this.terminalScreen.getText(), 500),
1553
+ });
1182
1554
  const submitStartedAt = Date.now();
1183
1555
  let lastNormalizedScreen = '';
1184
1556
  let lastScreenChangeAt = submitStartedAt;
@@ -1222,6 +1594,11 @@ export class ProviderCliAdapter implements CliAdapter {
1222
1594
  return this.ptyProcess.getMetadata();
1223
1595
  }
1224
1596
 
1597
+ updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
1598
+ if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
1599
+ this.ptyProcess.updateMeta(meta, replace);
1600
+ }
1601
+
1225
1602
  cancel(): void { this.shutdown(); }
1226
1603
 
1227
1604
  async saveAndStop(): Promise<void> {
@@ -1287,6 +1664,7 @@ export class ProviderCliAdapter implements CliAdapter {
1287
1664
  }
1288
1665
 
1289
1666
  shutdown(): void {
1667
+ this.clearIdleFinishCandidate('shutdown');
1290
1668
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
1291
1669
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1292
1670
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
@@ -1310,6 +1688,7 @@ export class ProviderCliAdapter implements CliAdapter {
1310
1688
  }
1311
1689
 
1312
1690
  detach(): void {
1691
+ this.clearIdleFinishCandidate('detach');
1313
1692
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
1314
1693
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1315
1694
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
@@ -1335,6 +1714,7 @@ export class ProviderCliAdapter implements CliAdapter {
1335
1714
  }
1336
1715
 
1337
1716
  clearHistory(): void {
1717
+ this.clearIdleFinishCandidate('clear_history');
1338
1718
  this.committedMessages = [];
1339
1719
  this.syncMessageViews();
1340
1720
  this.accumulatedBuffer = '';
@@ -1356,11 +1736,20 @@ export class ProviderCliAdapter implements CliAdapter {
1356
1736
  isReady(): boolean { return this.ready; }
1357
1737
 
1358
1738
  writeRaw(data: string): void {
1739
+ this.recordTrace('write_raw', {
1740
+ keys: JSON.stringify(data),
1741
+ length: data.length,
1742
+ });
1359
1743
  this.ptyProcess?.write(data);
1360
1744
  }
1361
1745
 
1362
1746
  resolveModal(buttonIndex: number): void {
1363
1747
  if (!this.ptyProcess || (this.currentStatus !== 'waiting_approval' && !this.activeModal)) return;
1748
+ this.clearIdleFinishCandidate('resolve_modal');
1749
+ this.recordTrace('resolve_modal', {
1750
+ buttonIndex,
1751
+ activeModal: this.activeModal,
1752
+ });
1364
1753
  this.activeModal = null;
1365
1754
  this.lastApprovalResolvedAt = Date.now();
1366
1755
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
@@ -1393,6 +1782,7 @@ export class ProviderCliAdapter implements CliAdapter {
1393
1782
  return {
1394
1783
  type: this.cliType,
1395
1784
  name: this.cliName,
1785
+ providerResolution: this.providerResolutionMeta,
1396
1786
  status: this.currentStatus,
1397
1787
  ready: this.ready,
1398
1788
  startupParseGate: this.startupParseGate,
@@ -1412,6 +1802,10 @@ export class ProviderCliAdapter implements CliAdapter {
1412
1802
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
1413
1803
  sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
1414
1804
  responseBuffer: this.responseBuffer.slice(-1000),
1805
+ lastOutputAt: this.lastOutputAt,
1806
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1807
+ lastScreenChangeAt: this.lastScreenChangeAt,
1808
+ lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
1415
1809
  isWaitingForResponse: this.isWaitingForResponse,
1416
1810
  activeModal: this.activeModal,
1417
1811
  lastApprovalResolvedAt: this.lastApprovalResolvedAt,
@@ -1423,6 +1817,8 @@ export class ProviderCliAdapter implements CliAdapter {
1423
1817
  resizeSuppressUntil: this.resizeSuppressUntil,
1424
1818
  hasCliScripts: this.hasCliScripts(),
1425
1819
  scriptNames: Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function'),
1820
+ traceSessionId: this.traceSessionId,
1821
+ traceEntryCount: this.traceEntries.length,
1426
1822
  statusHistory: this.statusHistory.slice(-30),
1427
1823
  timeouts: this.timeouts,
1428
1824
  pendingOutputParseBufferLength: this.pendingOutputParseBuffer.length,
@@ -1431,6 +1827,27 @@ export class ProviderCliAdapter implements CliAdapter {
1431
1827
  };
1432
1828
  }
1433
1829
 
1830
+ getTraceState(limit = 120): Record<string, any> {
1831
+ const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
1832
+ return {
1833
+ sessionId: this.traceSessionId,
1834
+ providerResolution: this.providerResolutionMeta,
1835
+ entryCount: this.traceEntries.length,
1836
+ entries: this.traceEntries.slice(-cappedLimit),
1837
+ screenText: this.summarizeTraceText(this.terminalScreen.getText(), 4000),
1838
+ recentOutputBuffer: this.summarizeTraceText(this.recentOutputBuffer, 1000),
1839
+ responseBuffer: this.summarizeTraceText(this.responseBuffer, 1200),
1840
+ status: this.currentStatus,
1841
+ activeModal: this.activeModal,
1842
+ currentTurnScope: this.currentTurnScope,
1843
+ messages: this.summarizeTraceMessages(this.committedMessages, 5),
1844
+ };
1845
+ }
1846
+
1847
+ getProviderResolutionMeta(): Record<string, any> {
1848
+ return { ...this.providerResolutionMeta };
1849
+ }
1850
+
1434
1851
  private respondToTerminalQueries(data: string): void {
1435
1852
  if (!this.ptyProcess || !data) return;
1436
1853