@adhdev/daemon-core 0.7.46 → 0.8.1

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.
@@ -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;
@@ -437,6 +473,12 @@ export class ProviderCliAdapter implements CliAdapter {
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
 
@@ -1094,17 +1429,24 @@ export class ProviderCliAdapter implements CliAdapter {
1094
1429
  }
1095
1430
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1096
1431
  if (this.isWaitingForResponse) return;
1432
+ await this.waitForInteractivePrompt();
1097
1433
 
1098
1434
  this.committedMessages.push({ role: 'user', content: text, timestamp: Date.now() });
1099
1435
  this.syncMessageViews();
1100
1436
  this.isWaitingForResponse = true;
1101
1437
  this.responseBuffer = '';
1438
+ this.clearIdleFinishCandidate('send_message');
1102
1439
  this.currentTurnScope = {
1103
1440
  prompt: text,
1104
1441
  startedAt: Date.now(),
1105
1442
  bufferStart: this.accumulatedBuffer.length,
1106
1443
  rawBufferStart: this.accumulatedRawBuffer.length,
1107
1444
  };
1445
+ this.recordTrace('send_message', {
1446
+ text: this.summarizeTraceText(text, 500),
1447
+ estimatedLines: estimatePromptDisplayLines(text),
1448
+ turnScope: this.currentTurnScope,
1449
+ });
1108
1450
  LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
1109
1451
  this.submitRetryUsed = false;
1110
1452
  this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
@@ -1135,6 +1477,11 @@ export class ProviderCliAdapter implements CliAdapter {
1135
1477
  const submit = () => {
1136
1478
  if (!this.ptyProcess) return;
1137
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
+ });
1138
1485
  this.ptyProcess.write(this.sendKey);
1139
1486
  const retrySubmitIfStuck = (attempt: number) => {
1140
1487
  this.submitRetryTimer = null;
@@ -1146,6 +1493,12 @@ export class ProviderCliAdapter implements CliAdapter {
1146
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;
1147
1494
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1148
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
+ });
1149
1502
  this.ptyProcess.write(this.sendKey);
1150
1503
  if (attempt >= 3) {
1151
1504
  this.submitRetryUsed = true;
@@ -1159,6 +1512,12 @@ export class ProviderCliAdapter implements CliAdapter {
1159
1512
 
1160
1513
  if (this.submitStrategy === 'immediate') {
1161
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
+ });
1162
1521
  this.ptyProcess.write(text + this.sendKey);
1163
1522
  this.submitRetryTimer = setTimeout(() => {
1164
1523
  this.submitRetryTimer = null;
@@ -1169,6 +1528,12 @@ export class ProviderCliAdapter implements CliAdapter {
1169
1528
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
1170
1529
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
1171
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
+ });
1172
1537
  this.ptyProcess.write(this.sendKey);
1173
1538
  this.submitRetryUsed = true;
1174
1539
  }, retryDelayMs);
@@ -1180,6 +1545,12 @@ export class ProviderCliAdapter implements CliAdapter {
1180
1545
  this.submitPendingUntil = Date.now() + submitDelayMs;
1181
1546
  }
1182
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
+ });
1183
1554
  const submitStartedAt = Date.now();
1184
1555
  let lastNormalizedScreen = '';
1185
1556
  let lastScreenChangeAt = submitStartedAt;
@@ -1293,6 +1664,7 @@ export class ProviderCliAdapter implements CliAdapter {
1293
1664
  }
1294
1665
 
1295
1666
  shutdown(): void {
1667
+ this.clearIdleFinishCandidate('shutdown');
1296
1668
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
1297
1669
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1298
1670
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
@@ -1316,6 +1688,7 @@ export class ProviderCliAdapter implements CliAdapter {
1316
1688
  }
1317
1689
 
1318
1690
  detach(): void {
1691
+ this.clearIdleFinishCandidate('detach');
1319
1692
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
1320
1693
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1321
1694
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
@@ -1341,6 +1714,7 @@ export class ProviderCliAdapter implements CliAdapter {
1341
1714
  }
1342
1715
 
1343
1716
  clearHistory(): void {
1717
+ this.clearIdleFinishCandidate('clear_history');
1344
1718
  this.committedMessages = [];
1345
1719
  this.syncMessageViews();
1346
1720
  this.accumulatedBuffer = '';
@@ -1362,11 +1736,20 @@ export class ProviderCliAdapter implements CliAdapter {
1362
1736
  isReady(): boolean { return this.ready; }
1363
1737
 
1364
1738
  writeRaw(data: string): void {
1739
+ this.recordTrace('write_raw', {
1740
+ keys: JSON.stringify(data),
1741
+ length: data.length,
1742
+ });
1365
1743
  this.ptyProcess?.write(data);
1366
1744
  }
1367
1745
 
1368
1746
  resolveModal(buttonIndex: number): void {
1369
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
+ });
1370
1753
  this.activeModal = null;
1371
1754
  this.lastApprovalResolvedAt = Date.now();
1372
1755
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
@@ -1399,6 +1782,7 @@ export class ProviderCliAdapter implements CliAdapter {
1399
1782
  return {
1400
1783
  type: this.cliType,
1401
1784
  name: this.cliName,
1785
+ providerResolution: this.providerResolutionMeta,
1402
1786
  status: this.currentStatus,
1403
1787
  ready: this.ready,
1404
1788
  startupParseGate: this.startupParseGate,
@@ -1418,6 +1802,10 @@ export class ProviderCliAdapter implements CliAdapter {
1418
1802
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
1419
1803
  sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
1420
1804
  responseBuffer: this.responseBuffer.slice(-1000),
1805
+ lastOutputAt: this.lastOutputAt,
1806
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1807
+ lastScreenChangeAt: this.lastScreenChangeAt,
1808
+ lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
1421
1809
  isWaitingForResponse: this.isWaitingForResponse,
1422
1810
  activeModal: this.activeModal,
1423
1811
  lastApprovalResolvedAt: this.lastApprovalResolvedAt,
@@ -1429,6 +1817,8 @@ export class ProviderCliAdapter implements CliAdapter {
1429
1817
  resizeSuppressUntil: this.resizeSuppressUntil,
1430
1818
  hasCliScripts: this.hasCliScripts(),
1431
1819
  scriptNames: Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function'),
1820
+ traceSessionId: this.traceSessionId,
1821
+ traceEntryCount: this.traceEntries.length,
1432
1822
  statusHistory: this.statusHistory.slice(-30),
1433
1823
  timeouts: this.timeouts,
1434
1824
  pendingOutputParseBufferLength: this.pendingOutputParseBuffer.length,
@@ -1437,6 +1827,27 @@ export class ProviderCliAdapter implements CliAdapter {
1437
1827
  };
1438
1828
  }
1439
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
+
1440
1851
  private respondToTerminalQueries(data: string): void {
1441
1852
  if (!this.ptyProcess || !data) return;
1442
1853