@adhdev/daemon-core 0.9.18 → 0.9.20

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.
@@ -43,6 +43,7 @@ import {
43
43
  type CliScripts,
44
44
  type CliSessionStatus,
45
45
  type CliTraceEntry,
46
+ type ParsedSession,
46
47
  } from './provider-cli-shared.js';
47
48
  import { buildChatMessage } from '../providers/chat-message-normalization.js';
48
49
  import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
@@ -95,16 +96,28 @@ interface IdleFinishCandidate {
95
96
 
96
97
  interface SettledEvalContext {
97
98
  now: number;
98
- screenText: string;
99
99
  modal: any;
100
- scriptStatus: string;
101
- parsedTranscript: any;
100
+ status: string;
102
101
  parsedMessages: CliChatMessage[];
103
102
  lastParsedAssistant: CliChatMessage | undefined;
104
- parsedShowsLiveAssistantProgress: boolean;
103
+ parsedStatus: string | null;
105
104
  prevStatus: string;
106
105
  }
107
106
 
107
+ interface SendMessageState {
108
+ text: string;
109
+ normalizedPromptSnippet: string;
110
+ submitDelayMs: number;
111
+ maxEchoWaitMs: number;
112
+ retryDelayMs: number;
113
+ didCommitUserTurn: boolean;
114
+ }
115
+
116
+ interface SendMessageCompletion {
117
+ resolveOnce: () => void;
118
+ rejectOnce: (error: unknown) => void;
119
+ }
120
+
108
121
  function normalizeComparableTranscriptText(value: unknown): string {
109
122
  return sanitizeTerminalText(String(value || ''))
110
123
  .replace(/\s+/g, ' ')
@@ -148,6 +161,15 @@ function parsedTranscriptIsRicherThanCommitted(
148
161
  return false;
149
162
  }
150
163
 
164
+ export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
165
+ if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
166
+ if (maxChars <= 0) return '';
167
+ if (chunk.length >= maxChars) return chunk.slice(-maxChars);
168
+ const keepFromCurrent = maxChars - chunk.length;
169
+ if (current.length <= keepFromCurrent) return current + chunk;
170
+ return current.slice(-keepFromCurrent) + chunk;
171
+ }
172
+
151
173
  // ─── Adapter ────────────────────────────────────────
152
174
 
153
175
  export class ProviderCliAdapter implements CliAdapter {
@@ -180,15 +202,17 @@ export class ProviderCliAdapter implements CliAdapter {
180
202
 
181
203
  // PTY I/O
182
204
  private onPtyDataCallback: ((data: string) => void) | null = null;
183
- private pendingOutputParseBuffer = '';
205
+ private pendingOutputParseChunks: string[] = [];
184
206
  private pendingOutputParseTimer: NodeJS.Timeout | null = null;
185
- private ptyOutputBuffer = '';
207
+ private ptyOutputChunks: string[] = [];
186
208
  private ptyOutputFlushTimer: NodeJS.Timeout | null = null;
187
209
  private pendingTerminalQueryTail = '';
188
210
  private lastOutputAt = 0;
189
211
  private lastNonEmptyOutputAt = 0;
190
212
  private lastScreenChangeAt = 0;
191
213
  private lastScreenSnapshot = '';
214
+ private lastScreenText = '';
215
+ private lastScreenSnapshotReadAt = Number.NEGATIVE_INFINITY;
192
216
 
193
217
  // Server log forwarding
194
218
  private serverConn: any = null;
@@ -254,6 +278,9 @@ export class ProviderCliAdapter implements CliAdapter {
254
278
  lastOutputAt: number;
255
279
  result: any;
256
280
  } | null = null;
281
+ private lastStatusHotPathParseAt = Number.NEGATIVE_INFINITY;
282
+ private static readonly STATUS_HOT_PATH_PARSE_MIN_INTERVAL_MS = 1000;
283
+ private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
257
284
  private static readonly MAX_TRACE_ENTRIES = 250;
258
285
 
259
286
  private readonly providerResolutionMeta: ProviderResolutionMeta;
@@ -265,6 +292,47 @@ export class ProviderCliAdapter implements CliAdapter {
265
292
  this.structuredMessages = [...this.committedMessages];
266
293
  }
267
294
 
295
+ private readTerminalScreenText(now = Date.now()): string {
296
+ const screenText = this.terminalScreen.getText() || '';
297
+ this.lastScreenText = screenText;
298
+ this.lastScreenSnapshotReadAt = now;
299
+ return screenText;
300
+ }
301
+
302
+ private shouldReadTerminalScreenSnapshot(now: number): boolean {
303
+ if (!this.lastScreenText) return true;
304
+ return (now - this.lastScreenSnapshotReadAt) >= ProviderCliAdapter.SCREEN_SNAPSHOT_MIN_INTERVAL_MS;
305
+ }
306
+
307
+ private resetTerminalScreen(rows?: number, cols?: number): void {
308
+ this.terminalScreen.reset(rows, cols);
309
+ this.lastScreenText = '';
310
+ this.lastScreenSnapshot = '';
311
+ this.lastScreenChangeAt = 0;
312
+ this.lastScreenSnapshotReadAt = Number.NEGATIVE_INFINITY;
313
+ }
314
+
315
+ private getFreshParsedStatusCache(): any | null {
316
+ const cached = this.parsedStatusCache;
317
+ if (
318
+ cached
319
+ && cached.committedMessagesRef === this.committedMessages
320
+ && cached.responseBuffer === this.responseBuffer
321
+ && cached.currentTurnScope === this.currentTurnScope
322
+ && cached.recentOutputBuffer === this.recentOutputBuffer
323
+ && cached.accumulatedBuffer === this.accumulatedBuffer
324
+ && cached.accumulatedRawBuffer === this.accumulatedRawBuffer
325
+ && cached.screenText === this.lastScreenText
326
+ && cached.currentStatus === this.currentStatus
327
+ && cached.activeModal === this.activeModal
328
+ && cached.cliName === this.cliName
329
+ && cached.lastOutputAt === this.lastOutputAt
330
+ ) {
331
+ return cached.result;
332
+ }
333
+ return null;
334
+ }
335
+
268
336
  private getIdleFinishConfirmMs(): number {
269
337
  return this.timeouts.idleFinishConfirm;
270
338
  }
@@ -445,9 +513,9 @@ export class ProviderCliAdapter implements CliAdapter {
445
513
  clearTimeout(this.pendingOutputParseTimer);
446
514
  this.pendingOutputParseTimer = null;
447
515
  }
448
- if (!this.pendingOutputParseBuffer) return;
449
- const rawData = this.pendingOutputParseBuffer;
450
- this.pendingOutputParseBuffer = '';
516
+ if (this.pendingOutputParseChunks.length === 0) return;
517
+ const rawData = this.pendingOutputParseChunks.join('');
518
+ this.pendingOutputParseChunks = [];
451
519
  this.handleOutput(rawData);
452
520
  }
453
521
 
@@ -509,7 +577,7 @@ export class ProviderCliAdapter implements CliAdapter {
509
577
  });
510
578
  }
511
579
 
512
- this.pendingOutputParseBuffer += data;
580
+ this.pendingOutputParseChunks.push(data);
513
581
  if (!this.pendingOutputParseTimer) {
514
582
  this.pendingOutputParseTimer = setTimeout(() => {
515
583
  this.pendingOutputParseTimer = null;
@@ -518,13 +586,13 @@ export class ProviderCliAdapter implements CliAdapter {
518
586
  }
519
587
 
520
588
  if (this.onPtyDataCallback) {
521
- this.ptyOutputBuffer += data;
589
+ this.ptyOutputChunks.push(data);
522
590
  if (!this.ptyOutputFlushTimer) {
523
591
  this.ptyOutputFlushTimer = setTimeout(() => {
524
- if (this.ptyOutputBuffer && this.onPtyDataCallback) {
525
- this.onPtyDataCallback(this.ptyOutputBuffer);
592
+ if (this.ptyOutputChunks.length > 0 && this.onPtyDataCallback) {
593
+ this.onPtyDataCallback(this.ptyOutputChunks.join(''));
526
594
  }
527
- this.ptyOutputBuffer = '';
595
+ this.ptyOutputChunks = [];
528
596
  this.ptyOutputFlushTimer = null;
529
597
  }, this.timeouts.ptyFlush);
530
598
  }
@@ -548,7 +616,7 @@ export class ProviderCliAdapter implements CliAdapter {
548
616
  this.startupBuffer = '';
549
617
  this.startupFirstOutputAt = 0;
550
618
  if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
551
- this.terminalScreen.reset(24, 80);
619
+ this.resetTerminalScreen(24, 80);
552
620
  this.pendingTerminalQueryTail = '';
553
621
  this.currentTurnScope = null;
554
622
  this.finishRetryCount = 0;
@@ -569,11 +637,14 @@ export class ProviderCliAdapter implements CliAdapter {
569
637
  this.terminalScreen.write(rawData);
570
638
  const cleanData = sanitizeTerminalText(rawData);
571
639
  const now = Date.now();
572
- const screenText = this.terminalScreen.getText();
573
- const normalizedScreenSnapshot = normalizeScreenSnapshot(screenText);
640
+ const shouldReadScreen = this.shouldReadTerminalScreenSnapshot(now);
641
+ const screenText = shouldReadScreen ? this.readTerminalScreenText(now) : this.lastScreenText;
642
+ const normalizedScreenSnapshot = shouldReadScreen
643
+ ? normalizeScreenSnapshot(screenText)
644
+ : this.lastScreenSnapshot;
574
645
  this.lastOutputAt = now;
575
646
  if (cleanData.trim()) this.lastNonEmptyOutputAt = now;
576
- if (normalizedScreenSnapshot !== this.lastScreenSnapshot) {
647
+ if (shouldReadScreen && normalizedScreenSnapshot !== this.lastScreenSnapshot) {
577
648
  this.lastScreenSnapshot = normalizedScreenSnapshot;
578
649
  this.lastScreenChangeAt = now;
579
650
  }
@@ -597,7 +668,7 @@ export class ProviderCliAdapter implements CliAdapter {
597
668
  }
598
669
 
599
670
  if (this.isWaitingForResponse && cleanData) {
600
- this.responseBuffer = (this.responseBuffer + cleanData).slice(-8000);
671
+ this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, 8000);
601
672
  }
602
673
 
603
674
  // Server log forwarding
@@ -610,11 +681,11 @@ export class ProviderCliAdapter implements CliAdapter {
610
681
  }
611
682
 
612
683
  // Rolling buffers
613
- this.recentOutputBuffer = (this.recentOutputBuffer + cleanData).slice(-1000);
614
684
  const prevAccumulatedLen = this.accumulatedBuffer.length;
615
685
  const prevAccumulatedRawLen = this.accumulatedRawBuffer.length;
616
- this.accumulatedBuffer = (this.accumulatedBuffer + cleanData).slice(-ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
617
- this.accumulatedRawBuffer = (this.accumulatedRawBuffer + rawData).slice(-ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
686
+ this.recentOutputBuffer = appendBoundedText(this.recentOutputBuffer, cleanData, 1000);
687
+ this.accumulatedBuffer = appendBoundedText(this.accumulatedBuffer, cleanData, ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
688
+ this.accumulatedRawBuffer = appendBoundedText(this.accumulatedRawBuffer, rawData, ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
618
689
  // Keep turn-scope offsets aligned with the truncated buffer so scoped
619
690
  // parses don't lose the beginning of a long turn (e.g. the Hermes
620
691
  // ╭─ opening line) when the rolling window sheds bytes.
@@ -629,18 +700,25 @@ export class ProviderCliAdapter implements CliAdapter {
629
700
  }
630
701
  }
631
702
 
632
- this.resolveStartupState('output');
703
+ this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
633
704
 
634
705
  // ─── Script-based status detection
635
706
  this.scheduleSettle();
636
707
  }
637
708
 
638
- private resolveStartupState(trigger: string): void {
709
+ private resolveStartupState(
710
+ trigger: string,
711
+ screenTextOverride?: string,
712
+ normalizedScreenOverride?: string,
713
+ nowOverride?: number,
714
+ ): void {
639
715
  if (!this.startupParseGate) return;
640
716
 
641
- const now = Date.now();
642
- const screenText = this.terminalScreen.getText() || '';
643
- const normalizedScreen = normalizeScreenSnapshot(screenText);
717
+ const now = typeof nowOverride === 'number' ? nowOverride : Date.now();
718
+ const screenText = typeof screenTextOverride === 'string' ? screenTextOverride : this.readTerminalScreenText();
719
+ const normalizedScreen = typeof normalizedScreenOverride === 'string'
720
+ ? normalizedScreenOverride
721
+ : normalizeScreenSnapshot(screenText);
644
722
  const hasStartupOutput = !!this.startupFirstOutputAt || !!normalizedScreen.trim();
645
723
  if (!hasStartupOutput) return;
646
724
 
@@ -886,48 +964,34 @@ export class ProviderCliAdapter implements CliAdapter {
886
964
  }, delayTime);
887
965
  return;
888
966
  }
889
- const tail = this.settledBuffer;
890
- const screenText = this.terminalScreen.getText() || '';
967
+
891
968
  this.resolveStartupState('settled');
892
- if (this.startupParseGate) {
893
- return;
894
- }
895
- const parsedTranscript = this.parseCurrentTranscript(
896
- this.committedMessages,
897
- this.responseBuffer,
898
- this.currentTurnScope,
899
- );
900
- const parsedModal = parsedTranscript?.activeModal && Array.isArray(parsedTranscript.activeModal.buttons) && parsedTranscript.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
901
- ? parsedTranscript.activeModal
902
- : null;
903
- const modal = this.runParseApproval(tail) || parsedModal;
904
- const rawScriptStatus = this.runDetectStatus(tail);
905
- const scriptStatus = parsedTranscript?.status === 'waiting_approval' && modal
906
- ? 'waiting_approval'
907
- : rawScriptStatus;
908
- const parsedMessages = Array.isArray(parsedTranscript?.messages)
909
- ? normalizeCliParsedMessages(parsedTranscript.messages, {
910
- committedMessages: this.committedMessages,
911
- scope: this.currentTurnScope,
912
- lastOutputAt: this.lastOutputAt,
913
- })
914
- : [];
915
- if (this.maybeCommitVisibleIdleTranscript(parsedTranscript)) {
916
- return;
917
- }
918
- const lastParsedAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
919
- const parsedShowsLiveAssistantProgress = parsedTranscript?.status === 'generating'
920
- && !!lastParsedAssistant
921
- && parsedMessages.length > this.committedMessages.length;
969
+ if (this.startupParseGate) return;
970
+
971
+ const session = this.runParseSession();
972
+ if (!session) return;
973
+
974
+ const { status, messages, modal, parsedStatus } = session;
975
+ const parsedMessages = normalizeCliParsedMessages(messages, {
976
+ committedMessages: this.committedMessages,
977
+ scope: this.currentTurnScope,
978
+ lastOutputAt: this.lastOutputAt,
979
+ });
980
+
981
+ if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages)) return;
982
+
983
+ const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
922
984
  const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || '');
985
+ const screenText = this.terminalScreen.getText() || '';
986
+
923
987
  this.recordTrace('settled', {
924
- tail: summarizeCliTraceText(tail, 500),
988
+ tail: summarizeCliTraceText(this.settledBuffer, 500),
925
989
  screenText: summarizeCliTraceText(screenText, 1200),
926
- detectStatus: scriptStatus,
927
- parsedStatus: parsedTranscript?.status || null,
990
+ detectStatus: status,
991
+ parsedStatus: parsedStatus || null,
928
992
  parsedMessageCount: parsedMessages.length,
929
993
  parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
930
- parsedActiveModal: parsedTranscript?.activeModal ?? null,
994
+ parsedActiveModal: modal,
931
995
  approval: modal,
932
996
  ...buildCliTraceParseSnapshot({
933
997
  accumulatedBuffer: this.accumulatedBuffer,
@@ -937,6 +1001,7 @@ export class ProviderCliAdapter implements CliAdapter {
937
1001
  scope: this.currentTurnScope,
938
1002
  }),
939
1003
  });
1004
+
940
1005
  if (
941
1006
  this.currentTurnScope
942
1007
  && !lastParsedAssistant
@@ -963,60 +1028,48 @@ export class ProviderCliAdapter implements CliAdapter {
963
1028
  }, this.timeouts.outputSettle + 150);
964
1029
  return;
965
1030
  }
1031
+
966
1032
  if (this.currentTurnScope && !lastParsedAssistant) {
967
1033
  LOG.info(
968
1034
  'CLI',
969
1035
  `[${this.cliType}] Settled without assistant: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
970
1036
  );
971
1037
  }
972
- if (!scriptStatus) return;
1038
+
1039
+ if (!status) return;
973
1040
 
974
1041
  const prevStatus = this.currentStatus;
975
- const ctx: SettledEvalContext = { now, screenText, modal, scriptStatus, parsedTranscript, parsedMessages, lastParsedAssistant, parsedShowsLiveAssistantProgress, prevStatus };
1042
+ const ctx: SettledEvalContext = { now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus };
976
1043
 
977
1044
  if (!this.applyPendingScriptStatusDebounce(ctx)) return;
978
1045
 
979
1046
  const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
980
1047
  LOG.info(
981
1048
  'CLI',
982
- `[${this.cliType}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} scriptStatus=${String(scriptStatus || '')} parsedStatus=${String(parsedTranscript?.status || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify(summarizeCliTraceText(lastParsedAssistant?.content || '', 120)).slice(0, 160)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 160)).slice(0, 220)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 160)).slice(0, 220)}`
1049
+ `[${this.cliType}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} status=${String(status || '')} parsedStatus=${String(parsedStatus || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify(summarizeCliTraceText(lastParsedAssistant?.content || '', 120)).slice(0, 160)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 160)).slice(0, 220)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 160)).slice(0, 220)}`
983
1050
  );
984
1051
 
985
1052
  const shouldHoldGenerating =
986
- scriptStatus === 'idle'
1053
+ status === 'idle'
987
1054
  && this.isWaitingForResponse
988
1055
  && !modal
989
1056
  && recentInteractiveActivity
990
- && !(parsedTranscript?.status === 'idle' && !!lastParsedAssistant);
991
-
992
- if (shouldHoldGenerating) {
993
- this.applyHoldGenerating(ctx, recentInteractiveActivity);
994
- return;
995
- }
996
-
997
- if (scriptStatus === 'waiting_approval') {
998
- this.applyWaitingApproval(ctx);
999
- return;
1000
- }
1001
-
1002
- if (scriptStatus === 'generating') {
1003
- this.applyGenerating(ctx);
1004
- return;
1005
- }
1057
+ && !(parsedStatus === 'idle' && !!lastParsedAssistant);
1006
1058
 
1007
- if (scriptStatus === 'idle') {
1008
- this.applyIdle(ctx, now);
1009
- }
1059
+ if (shouldHoldGenerating) { this.applyHoldGenerating(ctx, recentInteractiveActivity); return; }
1060
+ if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
1061
+ if (status === 'generating') { this.applyGenerating(ctx); return; }
1062
+ if (status === 'idle') { this.applyIdle(ctx, now); }
1010
1063
  }
1011
1064
 
1012
1065
  // Returns false if the caller should bail out (debounce pending).
1013
1066
  private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
1014
- const { now, scriptStatus, prevStatus } = ctx;
1067
+ const { now, status, prevStatus } = ctx;
1015
1068
  const shouldDebounce =
1016
1069
  prevStatus === 'idle'
1017
1070
  && !this.isWaitingForResponse
1018
1071
  && !this.currentTurnScope
1019
- && (scriptStatus === 'generating' || scriptStatus === 'waiting_approval');
1072
+ && (status === 'generating' || status === 'waiting_approval');
1020
1073
 
1021
1074
  if (!shouldDebounce) {
1022
1075
  this.pendingScriptStatus = null;
@@ -1034,8 +1087,8 @@ export class ProviderCliAdapter implements CliAdapter {
1034
1087
  }, delayMs);
1035
1088
  };
1036
1089
 
1037
- if (this.pendingScriptStatus !== scriptStatus) {
1038
- this.pendingScriptStatus = scriptStatus as 'generating' | 'waiting_approval';
1090
+ if (this.pendingScriptStatus !== status) {
1091
+ this.pendingScriptStatus = status as 'generating' | 'waiting_approval';
1039
1092
  this.pendingScriptStatusSince = now;
1040
1093
  armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
1041
1094
  return false;
@@ -1049,7 +1102,7 @@ export class ProviderCliAdapter implements CliAdapter {
1049
1102
  }
1050
1103
 
1051
1104
  private applyHoldGenerating(ctx: SettledEvalContext, recentInteractiveActivity: boolean): void {
1052
- const { scriptStatus } = ctx;
1105
+ const { status } = ctx;
1053
1106
  this.clearIdleFinishCandidate('hold_generating_recent_activity');
1054
1107
  this.setStatus('generating', 'recent_activity_hold');
1055
1108
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
@@ -1060,7 +1113,7 @@ export class ProviderCliAdapter implements CliAdapter {
1060
1113
  }
1061
1114
  }, this.timeouts.generatingIdle);
1062
1115
  this.recordTrace('hold_generating_recent_activity', {
1063
- scriptStatus,
1116
+ scriptStatus: status,
1064
1117
  recentInteractiveActivity,
1065
1118
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1066
1119
  lastScreenChangeAt: this.lastScreenChangeAt,
@@ -1113,8 +1166,9 @@ export class ProviderCliAdapter implements CliAdapter {
1113
1166
  }
1114
1167
 
1115
1168
  private applyGenerating(ctx: SettledEvalContext): void {
1116
- const { screenText, modal, parsedShowsLiveAssistantProgress, prevStatus } = ctx;
1169
+ const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
1117
1170
  this.clearIdleFinishCandidate('generating');
1171
+ const screenText = this.terminalScreen.getText() || '';
1118
1172
  const effectiveScreenText = screenText || this.accumulatedBuffer;
1119
1173
  const noActiveTurn = !this.currentTurnScope;
1120
1174
  const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText)
@@ -1122,6 +1176,9 @@ export class ProviderCliAdapter implements CliAdapter {
1122
1176
  && (/Update available!/i.test(screenText)
1123
1177
  || /\/effort/i.test(screenText)
1124
1178
  || /^.*➜\s+\S+/m.test(effectiveScreenText)));
1179
+ const parsedShowsLiveAssistantProgress = parsedStatus === 'generating'
1180
+ && !!lastParsedAssistant
1181
+ && parsedMessages.length > this.committedMessages.length;
1125
1182
  if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
1126
1183
  return;
1127
1184
  }
@@ -1148,7 +1205,7 @@ export class ProviderCliAdapter implements CliAdapter {
1148
1205
  }
1149
1206
 
1150
1207
  private applyIdle(ctx: SettledEvalContext, now: number): void {
1151
- const { screenText, modal, lastParsedAssistant, prevStatus } = ctx;
1208
+ const { modal, lastParsedAssistant, prevStatus } = ctx;
1152
1209
  if (prevStatus === 'waiting_approval') {
1153
1210
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1154
1211
  this.activeModal = null;
@@ -1281,30 +1338,24 @@ export class ProviderCliAdapter implements CliAdapter {
1281
1338
  this.onStatusChange?.();
1282
1339
  }
1283
1340
 
1284
- private maybeCommitVisibleIdleTranscript(parsed: any): boolean {
1341
+ private maybeCommitVisibleIdleTranscript(session: ParsedSession, parsedMessages: CliChatMessage[]): boolean {
1285
1342
  const allowImmediateScriptIdleCommit = this.provider.allowInputDuringGeneration === true;
1286
1343
  if (!allowImmediateScriptIdleCommit) return false;
1287
1344
  if (
1288
- !parsed
1289
- || !Array.isArray(parsed.messages)
1290
- || parsed.status !== 'idle'
1345
+ !session
1346
+ || session.status !== 'idle'
1291
1347
  || !this.isWaitingForResponse
1292
1348
  || !this.currentTurnScope
1293
1349
  || this.activeModal
1294
- || parsed.activeModal
1350
+ || session.modal
1295
1351
  ) {
1296
1352
  return false;
1297
1353
  }
1298
1354
 
1299
- const hydratedForIdleCommit = normalizeCliParsedMessages(parsed.messages, {
1300
- committedMessages: this.committedMessages,
1301
- scope: this.currentTurnScope,
1302
- lastOutputAt: this.lastOutputAt,
1303
- });
1304
- const visibleAssistant = [...hydratedForIdleCommit].reverse().find((message) => message.role === 'assistant' && message.content.trim());
1355
+ const visibleAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant' && m.content.trim());
1305
1356
  if (!visibleAssistant) return false;
1306
1357
 
1307
- this.committedMessages = hydratedForIdleCommit;
1358
+ this.committedMessages = parsedMessages;
1308
1359
  this.trimLastAssistantEcho(this.committedMessages, this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages));
1309
1360
  this.clearAllTimers();
1310
1361
  this.syncMessageViews();
@@ -1386,6 +1437,68 @@ export class ProviderCliAdapter implements CliAdapter {
1386
1437
 
1387
1438
  // ─── Script Execution ──────────────────────────
1388
1439
 
1440
+ private runParseSession(): ParsedSession | null {
1441
+ // Preferred: provider exposes a unified parseSession script
1442
+ if (typeof this.cliScripts?.parseSession === 'function') {
1443
+ try {
1444
+ const screenText = this.terminalScreen.getText();
1445
+ const tail = this.recentOutputBuffer.slice(-500);
1446
+ const input = buildCliParseInput({
1447
+ accumulatedBuffer: this.accumulatedBuffer,
1448
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1449
+ recentOutputBuffer: this.recentOutputBuffer,
1450
+ terminalScreenText: screenText,
1451
+ baseMessages: this.committedMessages,
1452
+ partialResponse: this.responseBuffer,
1453
+ isWaitingForResponse: this.isWaitingForResponse,
1454
+ scope: this.currentTurnScope,
1455
+ runtimeSettings: this.runtimeSettings,
1456
+ });
1457
+ const session = this.cliScripts.parseSession({ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
1458
+ this.parseErrorMessage = null;
1459
+ return session && typeof session === 'object' ? session : null;
1460
+ } catch (e: any) {
1461
+ const message = e?.message || String(e);
1462
+ this.parseErrorMessage = message;
1463
+ LOG.warn('CLI', `[${this.cliType}] parseSession error: ${message}`);
1464
+ return null;
1465
+ }
1466
+ }
1467
+ // Fallback: reconcile from the three individual scripts (for providers without parseSession)
1468
+ if (!this.cliScripts?.detectStatus && !this.cliScripts?.parseOutput) return null;
1469
+ try {
1470
+ const tail = this.settledBuffer;
1471
+ const parsedTranscript = this.parseCurrentTranscript(
1472
+ this.committedMessages,
1473
+ this.responseBuffer,
1474
+ this.currentTurnScope,
1475
+ );
1476
+ const parsedModal = parsedTranscript?.activeModal
1477
+ && Array.isArray(parsedTranscript.activeModal.buttons)
1478
+ && parsedTranscript.activeModal.buttons.some((b: any) => typeof b === 'string' && b.trim())
1479
+ ? parsedTranscript.activeModal
1480
+ : null;
1481
+ const approval = this.runParseApproval(tail);
1482
+ const modal = approval || parsedModal;
1483
+ const rawStatus = this.runDetectStatus(tail);
1484
+ const parsedStatus = typeof parsedTranscript?.status === 'string' ? parsedTranscript.status : null;
1485
+ const effectiveStatus = (parsedStatus === 'waiting_approval' && modal)
1486
+ ? 'waiting_approval'
1487
+ : (rawStatus || parsedStatus || 'idle');
1488
+ return {
1489
+ status: effectiveStatus,
1490
+ messages: Array.isArray(parsedTranscript?.messages) ? parsedTranscript.messages : [],
1491
+ modal,
1492
+ parsedStatus,
1493
+ };
1494
+ } catch (e: any) {
1495
+ const message = e?.message || String(e);
1496
+ this.parseErrorMessage = message;
1497
+ LOG.warn('CLI', `[${this.cliType}] parseSession fallback error: ${message}`);
1498
+ return null;
1499
+ }
1500
+ }
1501
+
1389
1502
  private runDetectStatus(text: string): string | null {
1390
1503
  if (!this.cliScripts?.detectStatus) return null;
1391
1504
  try {
@@ -1478,18 +1591,25 @@ export class ProviderCliAdapter implements CliAdapter {
1478
1591
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1479
1592
  let effectiveModal = startupModal || this.activeModal;
1480
1593
  if (!startupModal && !effectiveModal && typeof this.cliScripts?.parseOutput === 'function') {
1481
- try {
1482
- const parsed = this.getScriptParsedStatus();
1483
- const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
1484
- && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
1485
- ? parsed.activeModal
1486
- : null;
1487
- if (parsed?.status === 'waiting_approval' && parsedModal) {
1488
- effectiveStatus = 'waiting_approval';
1489
- effectiveModal = parsedModal;
1594
+ let parsed = this.getFreshParsedStatusCache();
1595
+ if (!parsed && effectiveStatus !== 'idle') {
1596
+ const now = Date.now();
1597
+ if ((now - this.lastStatusHotPathParseAt) >= ProviderCliAdapter.STATUS_HOT_PATH_PARSE_MIN_INTERVAL_MS) {
1598
+ this.lastStatusHotPathParseAt = now;
1599
+ try {
1600
+ parsed = this.getScriptParsedStatus();
1601
+ } catch {
1602
+ // Ignore parse errors here; getScriptParsedStatus surfaces them on richer callers.
1603
+ }
1490
1604
  }
1491
- } catch {
1492
- // Ignore parse errors here; getScriptParsedStatus surfaces them on richer callers.
1605
+ }
1606
+ const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
1607
+ && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
1608
+ ? parsed.activeModal
1609
+ : null;
1610
+ if (parsed?.status === 'waiting_approval' && parsedModal) {
1611
+ effectiveStatus = 'waiting_approval';
1612
+ effectiveModal = parsedModal;
1493
1613
  }
1494
1614
  }
1495
1615
  return {
@@ -1529,7 +1649,7 @@ export class ProviderCliAdapter implements CliAdapter {
1529
1649
  * Called by command handler / dashboard for rich content rendering.
1530
1650
  */
1531
1651
  getScriptParsedStatus(): any {
1532
- const screenText = this.terminalScreen.getText();
1652
+ const screenText = this.readTerminalScreenText();
1533
1653
  const cached = this.parsedStatusCache;
1534
1654
  if (
1535
1655
  cached
@@ -1566,8 +1686,21 @@ export class ProviderCliAdapter implements CliAdapter {
1566
1686
  this.onStatusChange?.();
1567
1687
  }
1568
1688
  }
1569
- if (this.maybeCommitVisibleIdleTranscript(parsed)) {
1570
- return this.getScriptParsedStatus();
1689
+ if (parsed && Array.isArray(parsed.messages)) {
1690
+ const hydratedForCommit = normalizeCliParsedMessages(parsed.messages, {
1691
+ committedMessages: this.committedMessages,
1692
+ scope: this.currentTurnScope,
1693
+ lastOutputAt: this.lastOutputAt,
1694
+ });
1695
+ const fakeSession: ParsedSession = {
1696
+ status: parsed.status || 'idle',
1697
+ messages: parsed.messages,
1698
+ modal: parsedModal,
1699
+ parsedStatus: parsed.status || null,
1700
+ };
1701
+ if (this.maybeCommitVisibleIdleTranscript(fakeSession, hydratedForCommit)) {
1702
+ return this.getScriptParsedStatus();
1703
+ }
1571
1704
  }
1572
1705
  const shouldPreferCommittedMessages =
1573
1706
  !this.currentTurnScope
@@ -1796,6 +1929,18 @@ export class ProviderCliAdapter implements CliAdapter {
1796
1929
  await this.sendMessage(promptText);
1797
1930
  }
1798
1931
 
1932
+ private isSubmitStuck(normalizedPromptSnippet: string): boolean {
1933
+ if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
1934
+ if (this.hasActionableApproval()) return false;
1935
+ if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return false;
1936
+ const screenText = this.terminalScreen.getText();
1937
+ if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
1938
+ const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
1939
+ if (liveApproval) return false;
1940
+ const liveStatus = this.runDetectStatus(screenText) || this.runDetectStatus(this.recentOutputBuffer);
1941
+ return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
1942
+ }
1943
+
1799
1944
  private async writeToPty(data: string): Promise<void> {
1800
1945
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1801
1946
  await this.ptyProcess.write(data);
@@ -1812,6 +1957,135 @@ export class ProviderCliAdapter implements CliAdapter {
1812
1957
  if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1813
1958
  }
1814
1959
 
1960
+ private commitSendUserTurn(state: SendMessageState): void {
1961
+ if (state.didCommitUserTurn) return;
1962
+ state.didCommitUserTurn = true;
1963
+ this.committedMessages.push({ role: 'user', content: state.text, timestamp: Date.now() });
1964
+ this.syncMessageViews();
1965
+ }
1966
+
1967
+ private armResponseTimeout(): void {
1968
+ if (this.responseTimeout) clearTimeout(this.responseTimeout);
1969
+ this.responseTimeout = setTimeout(() => {
1970
+ if (this.isWaitingForResponse) this.finishResponse();
1971
+ }, this.timeouts.maxResponse);
1972
+ }
1973
+
1974
+ private writeSubmitKeyForRetry(mode: string): void {
1975
+ void this.writeToPty(this.sendKey).catch((error) => {
1976
+ LOG.warn('CLI', `[${this.cliType}] ${mode} write failed: ${error?.message || error}`);
1977
+ });
1978
+ }
1979
+
1980
+ private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
1981
+ this.submitRetryTimer = null;
1982
+ if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
1983
+ const screenText = this.terminalScreen.getText();
1984
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1985
+ LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
1986
+ this.recordTrace('submit_write', {
1987
+ mode: 'submit_retry',
1988
+ attempt,
1989
+ sendKey: this.sendKey,
1990
+ screenText: summarizeCliTraceText(screenText, 500),
1991
+ });
1992
+ this.writeSubmitKeyForRetry('submit_retry');
1993
+ if (attempt >= 3) { this.submitRetryUsed = true; return; }
1994
+ this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
1995
+ }
1996
+
1997
+ private retryImmediateSubmitIfStuck(state: SendMessageState): void {
1998
+ this.submitRetryTimer = null;
1999
+ if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
2000
+ const screenText = this.terminalScreen.getText();
2001
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
2002
+ LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
2003
+ this.recordTrace('submit_write', {
2004
+ mode: 'immediate_retry',
2005
+ attempt: 1,
2006
+ sendKey: this.sendKey,
2007
+ screenText: summarizeCliTraceText(screenText, 500),
2008
+ });
2009
+ this.writeSubmitKeyForRetry('immediate_retry');
2010
+ this.submitRetryUsed = true;
2011
+ }
2012
+
2013
+ private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
2014
+ if (!this.ptyProcess) {
2015
+ completion.resolveOnce();
2016
+ return;
2017
+ }
2018
+ this.submitPendingUntil = 0;
2019
+ const screenText = this.terminalScreen.getText();
2020
+ this.recordTrace('submit_write', {
2021
+ mode: 'submit_key',
2022
+ sendKey: this.sendKey,
2023
+ screenText: summarizeCliTraceText(screenText, 500),
2024
+ });
2025
+ void this.writeToPty(this.sendKey).then(() => {
2026
+ this.commitSendUserTurn(state);
2027
+ this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
2028
+ this.armResponseTimeout();
2029
+ completion.resolveOnce();
2030
+ }, completion.rejectOnce);
2031
+ }
2032
+
2033
+ private submitImmediatePrompt(state: SendMessageState, completion: SendMessageCompletion): void {
2034
+ this.submitPendingUntil = 0;
2035
+ this.recordTrace('submit_write', {
2036
+ mode: 'immediate',
2037
+ text: summarizeCliTraceText(state.text, 500),
2038
+ sendKey: this.sendKey,
2039
+ screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
2040
+ });
2041
+ void this.writeToPty(state.text + this.sendKey).then(() => {
2042
+ this.commitSendUserTurn(state);
2043
+ this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
2044
+ this.armResponseTimeout();
2045
+ completion.resolveOnce();
2046
+ }, completion.rejectOnce);
2047
+ }
2048
+
2049
+ private waitForEchoAndSubmit(
2050
+ state: SendMessageState,
2051
+ completion: SendMessageCompletion,
2052
+ submitStartedAt: number,
2053
+ lastNormalizedScreen = '',
2054
+ lastScreenChangeAt = submitStartedAt,
2055
+ ): void {
2056
+ if (!this.ptyProcess) {
2057
+ completion.resolveOnce();
2058
+ return;
2059
+ }
2060
+ const now = Date.now();
2061
+ const elapsed = now - submitStartedAt;
2062
+ const screenText = this.terminalScreen.getText();
2063
+ const normalizedScreen = normalizePromptText(screenText);
2064
+ const nextScreenChangeAt = normalizedScreen !== lastNormalizedScreen ? now : lastScreenChangeAt;
2065
+ const echoVisible = !state.normalizedPromptSnippet || promptLikelyVisible(screenText, state.normalizedPromptSnippet);
2066
+
2067
+ if (echoVisible) {
2068
+ const screenSettled = (now - nextScreenChangeAt) >= 500;
2069
+ if (elapsed >= state.submitDelayMs && screenSettled) {
2070
+ this.submitSendKey(state, completion);
2071
+ return;
2072
+ }
2073
+ }
2074
+
2075
+ if (elapsed >= state.maxEchoWaitMs) {
2076
+ this.submitSendKey(state, completion);
2077
+ return;
2078
+ }
2079
+
2080
+ setTimeout(() => this.waitForEchoAndSubmit(
2081
+ state,
2082
+ completion,
2083
+ submitStartedAt,
2084
+ normalizedScreen,
2085
+ nextScreenChangeAt,
2086
+ ), 50);
2087
+ }
2088
+
1815
2089
  async sendMessage(text: string): Promise<void> {
1816
2090
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1817
2091
  const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
@@ -1895,12 +2169,13 @@ export class ProviderCliAdapter implements CliAdapter {
1895
2169
  const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
1896
2170
  const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
1897
2171
  const retryDelayMs = Math.max(350, Math.min(1500, Math.max(this.sendDelayMs, submitDelayMs)));
1898
- let didCommitUserTurn = false;
1899
- const commitUserTurn = () => {
1900
- if (didCommitUserTurn) return;
1901
- didCommitUserTurn = true;
1902
- this.committedMessages.push({ role: 'user', content: text, timestamp: Date.now() });
1903
- this.syncMessageViews();
2172
+ const sendState: SendMessageState = {
2173
+ text,
2174
+ normalizedPromptSnippet,
2175
+ submitDelayMs,
2176
+ maxEchoWaitMs,
2177
+ retryDelayMs,
2178
+ didCommitUserTurn: false,
1904
2179
  };
1905
2180
  if (this.settleTimer) {
1906
2181
  clearTimeout(this.settleTimer);
@@ -1908,112 +2183,24 @@ export class ProviderCliAdapter implements CliAdapter {
1908
2183
  }
1909
2184
  this.responseEpoch += 1;
1910
2185
  this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
1911
- const startResponseTimeout = () => {
1912
- if (this.responseTimeout) clearTimeout(this.responseTimeout);
1913
- this.responseTimeout = setTimeout(() => {
1914
- if (this.isWaitingForResponse) this.finishResponse();
1915
- }, this.timeouts.maxResponse);
1916
- };
1917
2186
  await new Promise<void>((resolve, reject) => {
1918
2187
  let resolved = false;
1919
- const resolveOnce = () => {
1920
- if (resolved) return;
1921
- resolved = true;
1922
- resolve();
1923
- };
1924
- const rejectOnce = (error: unknown) => {
1925
- if (resolved) return;
1926
- this.resetPendingSendState('send_write_failed');
1927
- resolved = true;
1928
- reject(error);
1929
- };
1930
- const writeRetryKey = (mode: string) => {
1931
- void this.writeToPty(this.sendKey).catch((error) => {
1932
- LOG.warn('CLI', `[${this.cliType}] ${mode} write failed: ${error?.message || error}`);
1933
- });
1934
- };
1935
-
1936
- const submit = () => {
1937
- if (!this.ptyProcess) {
1938
- resolveOnce();
1939
- return;
1940
- }
1941
- this.submitPendingUntil = 0;
1942
- const screenText = this.terminalScreen.getText();
1943
- this.recordTrace('submit_write', {
1944
- mode: 'submit_key',
1945
- sendKey: this.sendKey,
1946
- screenText: summarizeCliTraceText(screenText, 500),
1947
- });
1948
- const retrySubmitIfStuck = (attempt: number) => {
1949
- this.submitRetryTimer = null;
1950
- if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
1951
- if (this.hasActionableApproval()) return;
1952
- if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return;
1953
- const screenText = this.terminalScreen.getText();
1954
- if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
1955
- const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
1956
- if (liveApproval) return;
1957
- const liveStatus = this.runDetectStatus(screenText) || this.runDetectStatus(this.recentOutputBuffer);
1958
- if (liveStatus === 'generating' || liveStatus === 'waiting_approval') return;
1959
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1960
- LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
1961
- this.recordTrace('submit_write', {
1962
- mode: 'submit_retry',
1963
- attempt,
1964
- sendKey: this.sendKey,
1965
- screenText: summarizeCliTraceText(screenText, 500),
1966
- });
1967
- writeRetryKey('submit_retry');
1968
- if (attempt >= 3) {
1969
- this.submitRetryUsed = true;
1970
- return;
1971
- }
1972
- this.submitRetryTimer = setTimeout(() => retrySubmitIfStuck(attempt + 1), retryDelayMs);
1973
- };
1974
- void this.writeToPty(this.sendKey).then(() => {
1975
- commitUserTurn();
1976
- this.submitRetryTimer = setTimeout(() => retrySubmitIfStuck(1), retryDelayMs);
1977
- startResponseTimeout();
1978
- resolveOnce();
1979
- }, rejectOnce);
2188
+ const completion: SendMessageCompletion = {
2189
+ resolveOnce: () => {
2190
+ if (resolved) return;
2191
+ resolved = true;
2192
+ resolve();
2193
+ },
2194
+ rejectOnce: (error: unknown) => {
2195
+ if (resolved) return;
2196
+ this.resetPendingSendState('send_write_failed');
2197
+ resolved = true;
2198
+ reject(error);
2199
+ },
1980
2200
  };
1981
2201
 
1982
2202
  if (this.submitStrategy === 'immediate') {
1983
- this.submitPendingUntil = 0;
1984
- this.recordTrace('submit_write', {
1985
- mode: 'immediate',
1986
- text: summarizeCliTraceText(text, 500),
1987
- sendKey: this.sendKey,
1988
- screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
1989
- });
1990
- void this.writeToPty(text + this.sendKey).then(() => {
1991
- commitUserTurn();
1992
- this.submitRetryTimer = setTimeout(() => {
1993
- this.submitRetryTimer = null;
1994
- if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
1995
- if (this.hasActionableApproval()) return;
1996
- if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return;
1997
- const screenText = this.terminalScreen.getText();
1998
- if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
1999
- const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
2000
- if (liveApproval) return;
2001
- const liveStatus = this.runDetectStatus(screenText) || this.runDetectStatus(this.recentOutputBuffer);
2002
- if (liveStatus === 'generating' || liveStatus === 'waiting_approval') return;
2003
- LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
2004
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
2005
- this.recordTrace('submit_write', {
2006
- mode: 'immediate_retry',
2007
- attempt: 1,
2008
- sendKey: this.sendKey,
2009
- screenText: summarizeCliTraceText(screenText, 500),
2010
- });
2011
- writeRetryKey('immediate_retry');
2012
- this.submitRetryUsed = true;
2013
- }, retryDelayMs);
2014
- startResponseTimeout();
2015
- resolveOnce();
2016
- }, rejectOnce);
2203
+ this.submitImmediatePrompt(sendState, completion);
2017
2204
  return;
2018
2205
  }
2019
2206
 
@@ -2027,39 +2214,10 @@ export class ProviderCliAdapter implements CliAdapter {
2027
2214
  screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
2028
2215
  });
2029
2216
  const submitStartedAt = Date.now();
2030
- let lastNormalizedScreen = '';
2031
- let lastScreenChangeAt = submitStartedAt;
2032
- const waitForEchoAndSubmit = () => {
2033
- if (!this.ptyProcess) {
2034
- resolveOnce();
2035
- return;
2036
- }
2037
- const now = Date.now();
2038
- const elapsed = now - submitStartedAt;
2039
- const screenText = this.terminalScreen.getText();
2040
- const normalizedScreen = normalizePromptText(screenText);
2041
- if (normalizedScreen !== lastNormalizedScreen) {
2042
- lastNormalizedScreen = normalizedScreen;
2043
- lastScreenChangeAt = now;
2044
- }
2045
- const echoVisible = !normalizedPromptSnippet || promptLikelyVisible(screenText, normalizedPromptSnippet);
2046
-
2047
- if (echoVisible) {
2048
- const screenSettled = (now - lastScreenChangeAt) >= 500;
2049
- if (elapsed >= submitDelayMs && screenSettled) {
2050
- submit();
2051
- return;
2052
- }
2053
- }
2054
-
2055
- if (elapsed >= maxEchoWaitMs) {
2056
- submit();
2057
- return;
2058
- }
2059
-
2060
- setTimeout(waitForEchoAndSubmit, 50);
2061
- };
2062
- void this.writeToPty(text).then(() => waitForEchoAndSubmit(), rejectOnce);
2217
+ void this.writeToPty(text).then(
2218
+ () => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
2219
+ completion.rejectOnce,
2220
+ );
2063
2221
  });
2064
2222
  }
2065
2223
 
@@ -2148,9 +2306,9 @@ export class ProviderCliAdapter implements CliAdapter {
2148
2306
  shutdown(): void {
2149
2307
  this.clearIdleFinishCandidate('shutdown');
2150
2308
  this.clearAllTimers();
2151
- this.pendingOutputParseBuffer = '';
2309
+ this.pendingOutputParseChunks = [];
2152
2310
  this.pendingTerminalQueryTail = '';
2153
- this.ptyOutputBuffer = '';
2311
+ this.ptyOutputChunks = [];
2154
2312
  this.finishRetryCount = 0;
2155
2313
  if (this.ptyProcess) {
2156
2314
  this.ptyProcess.write('\x03');
@@ -2169,9 +2327,9 @@ export class ProviderCliAdapter implements CliAdapter {
2169
2327
  detach(): void {
2170
2328
  this.clearIdleFinishCandidate('detach');
2171
2329
  this.clearAllTimers();
2172
- this.pendingOutputParseBuffer = '';
2330
+ this.pendingOutputParseChunks = [];
2173
2331
  this.pendingTerminalQueryTail = '';
2174
- this.ptyOutputBuffer = '';
2332
+ this.ptyOutputChunks = [];
2175
2333
  this.finishRetryCount = 0;
2176
2334
  if (this.ptyProcess) {
2177
2335
  try {
@@ -2199,13 +2357,13 @@ export class ProviderCliAdapter implements CliAdapter {
2199
2357
  this.submitRetryUsed = false;
2200
2358
  this.submitRetryPromptSnippet = '';
2201
2359
  if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
2202
- this.pendingOutputParseBuffer = '';
2360
+ this.pendingOutputParseChunks = [];
2203
2361
  this.pendingTerminalQueryTail = '';
2204
2362
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
2205
- this.ptyOutputBuffer = '';
2363
+ this.ptyOutputChunks = [];
2206
2364
  if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2207
2365
  this.finishRetryCount = 0;
2208
- this.terminalScreen.reset();
2366
+ this.resetTerminalScreen();
2209
2367
  this.ptyProcess?.clearBuffer?.();
2210
2368
  this.onStatusChange?.();
2211
2369
  }
@@ -2326,7 +2484,7 @@ export class ProviderCliAdapter implements CliAdapter {
2326
2484
  traceEntryCount: this.traceEntries.length,
2327
2485
  statusHistory: this.statusHistory.slice(-30),
2328
2486
  timeouts: this.timeouts,
2329
- pendingOutputParseBufferLength: this.pendingOutputParseBuffer.length,
2487
+ pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
2330
2488
  pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
2331
2489
  ptyAlive: !!this.ptyProcess,
2332
2490
  };