@adhdev/daemon-core 0.9.19 → 0.9.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +22 -2
- package/dist/cli-adapters/provider-cli-shared.d.ts +13 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +371 -249
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +371 -249
- package/dist/index.mjs.map +1 -1
- package/dist/shared-types.d.ts +13 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +426 -268
- package/src/cli-adapters/provider-cli-shared.ts +8 -0
- package/src/commands/chat-commands.ts +9 -8
- package/src/index.d.ts +1 -1
- package/src/index.ts +1 -0
- package/src/shared-types.d.ts +13 -1
- package/src/shared-types.ts +15 -1
- package/src/status/builders.ts +8 -8
- package/src/status/reporter.ts +7 -11
- package/src/status/snapshot.ts +21 -5
|
@@ -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
|
-
|
|
101
|
-
parsedTranscript: any;
|
|
100
|
+
status: string;
|
|
102
101
|
parsedMessages: CliChatMessage[];
|
|
103
102
|
lastParsedAssistant: CliChatMessage | undefined;
|
|
104
|
-
|
|
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
|
|
205
|
+
private pendingOutputParseChunks: string[] = [];
|
|
184
206
|
private pendingOutputParseTimer: NodeJS.Timeout | null = null;
|
|
185
|
-
private
|
|
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 (
|
|
449
|
-
const rawData = this.
|
|
450
|
-
this.
|
|
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.
|
|
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.
|
|
589
|
+
this.ptyOutputChunks.push(data);
|
|
522
590
|
if (!this.ptyOutputFlushTimer) {
|
|
523
591
|
this.ptyOutputFlushTimer = setTimeout(() => {
|
|
524
|
-
if (this.
|
|
525
|
-
this.onPtyDataCallback(this.
|
|
592
|
+
if (this.ptyOutputChunks.length > 0 && this.onPtyDataCallback) {
|
|
593
|
+
this.onPtyDataCallback(this.ptyOutputChunks.join(''));
|
|
526
594
|
}
|
|
527
|
-
this.
|
|
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.
|
|
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
|
|
573
|
-
const
|
|
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
|
|
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.
|
|
617
|
-
this.
|
|
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(
|
|
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.
|
|
643
|
-
const normalizedScreen =
|
|
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
|
-
|
|
890
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
967
|
+
|
|
891
968
|
this.resolveStartupState('settled');
|
|
892
|
-
if (this.startupParseGate)
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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(
|
|
988
|
+
tail: summarizeCliTraceText(this.settledBuffer, 500),
|
|
925
989
|
screenText: summarizeCliTraceText(screenText, 1200),
|
|
926
|
-
detectStatus:
|
|
927
|
-
parsedStatus:
|
|
990
|
+
detectStatus: status,
|
|
991
|
+
parsedStatus: parsedStatus || null,
|
|
928
992
|
parsedMessageCount: parsedMessages.length,
|
|
929
993
|
parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
|
|
930
|
-
parsedActiveModal:
|
|
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
|
-
|
|
1038
|
+
|
|
1039
|
+
if (!status) return;
|
|
973
1040
|
|
|
974
1041
|
const prevStatus = this.currentStatus;
|
|
975
|
-
const ctx: SettledEvalContext = { now,
|
|
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)}
|
|
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
|
-
|
|
1053
|
+
status === 'idle'
|
|
987
1054
|
&& this.isWaitingForResponse
|
|
988
1055
|
&& !modal
|
|
989
1056
|
&& recentInteractiveActivity
|
|
990
|
-
&& !(
|
|
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 (
|
|
1008
|
-
|
|
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,
|
|
1067
|
+
const { now, status, prevStatus } = ctx;
|
|
1015
1068
|
const shouldDebounce =
|
|
1016
1069
|
prevStatus === 'idle'
|
|
1017
1070
|
&& !this.isWaitingForResponse
|
|
1018
1071
|
&& !this.currentTurnScope
|
|
1019
|
-
&& (
|
|
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 !==
|
|
1038
|
-
this.pendingScriptStatus =
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
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
|
-
!
|
|
1289
|
-
||
|
|
1290
|
-
|| parsed.status !== 'idle'
|
|
1345
|
+
!session
|
|
1346
|
+
|| session.status !== 'idle'
|
|
1291
1347
|
|| !this.isWaitingForResponse
|
|
1292
1348
|
|| !this.currentTurnScope
|
|
1293
1349
|
|| this.activeModal
|
|
1294
|
-
||
|
|
1350
|
+
|| session.modal
|
|
1295
1351
|
) {
|
|
1296
1352
|
return false;
|
|
1297
1353
|
}
|
|
1298
1354
|
|
|
1299
|
-
const
|
|
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 =
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
-
}
|
|
1492
|
-
|
|
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.
|
|
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 (
|
|
1570
|
-
|
|
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
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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.
|
|
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
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
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.
|
|
2309
|
+
this.pendingOutputParseChunks = [];
|
|
2152
2310
|
this.pendingTerminalQueryTail = '';
|
|
2153
|
-
this.
|
|
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.
|
|
2330
|
+
this.pendingOutputParseChunks = [];
|
|
2173
2331
|
this.pendingTerminalQueryTail = '';
|
|
2174
|
-
this.
|
|
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.
|
|
2360
|
+
this.pendingOutputParseChunks = [];
|
|
2203
2361
|
this.pendingTerminalQueryTail = '';
|
|
2204
2362
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2205
|
-
this.
|
|
2363
|
+
this.ptyOutputChunks = [];
|
|
2206
2364
|
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
2207
2365
|
this.finishRetryCount = 0;
|
|
2208
|
-
this.
|
|
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.
|
|
2487
|
+
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2330
2488
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
2331
2489
|
ptyAlive: !!this.ptyProcess,
|
|
2332
2490
|
};
|