@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.137
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/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +154 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +73 -74
- package/dist/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2591 -1966
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2594 -1974
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +54 -0
- package/dist/mesh/mesh-active-work.d.ts +7 -1
- package/dist/mesh/mesh-events.d.ts +10 -4
- package/dist/mesh/mesh-ledger.d.ts +21 -1
- package/dist/mesh/mesh-refine-status.d.ts +2 -3
- package/dist/mesh/mesh-work-queue.d.ts +17 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
- package/dist/repo-mesh-types.d.ts +5 -0
- package/package.json +1 -1
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +957 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
- package/src/cli-adapters/provider-cli-adapter.ts +365 -1397
- package/src/cli-adapters/provider-cli-shared.ts +4 -0
- package/src/commands/chat-commands.ts +17 -1
- package/src/commands/router.ts +8 -0
- package/src/config/chat-history.ts +7 -3
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +3 -2
- package/src/mesh/beads-db.ts +305 -2
- package/src/mesh/coordinator-prompt.ts +12 -17
- package/src/mesh/mesh-active-work.ts +162 -59
- package/src/mesh/mesh-events.ts +198 -53
- package/src/mesh/mesh-ledger.ts +321 -105
- package/src/mesh/mesh-refine-status.ts +2 -3
- package/src/mesh/mesh-work-queue.ts +116 -120
- package/src/mesh/worktree-bootstrap-config.ts +17 -4
- package/src/providers/provider-schema.ts +2 -0
- package/src/repo-mesh-types.ts +10 -0
|
@@ -44,6 +44,13 @@ import {
|
|
|
44
44
|
type CliTraceEntry,
|
|
45
45
|
type ParsedSession,
|
|
46
46
|
} from './provider-cli-shared.js';
|
|
47
|
+
import { CliScriptRunner } from './cli-script-runner.js';
|
|
48
|
+
import {
|
|
49
|
+
CliStateEngine,
|
|
50
|
+
type CliBufferSnapshot,
|
|
51
|
+
type CliTransportAccess,
|
|
52
|
+
type CliStateEngineCallbacks,
|
|
53
|
+
} from './cli-state-engine.js';
|
|
47
54
|
import {
|
|
48
55
|
buildCliParseInput,
|
|
49
56
|
buildCliTraceParseSnapshot,
|
|
@@ -78,24 +85,6 @@ export {
|
|
|
78
85
|
} from './provider-cli-shared.js';
|
|
79
86
|
|
|
80
87
|
|
|
81
|
-
interface IdleFinishCandidate {
|
|
82
|
-
armedAt: number;
|
|
83
|
-
lastOutputAt: number;
|
|
84
|
-
lastScreenChangeAt: number;
|
|
85
|
-
responseEpoch: number;
|
|
86
|
-
assistantLength: number;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface SettledEvalContext {
|
|
90
|
-
now: number;
|
|
91
|
-
modal: any;
|
|
92
|
-
status: string;
|
|
93
|
-
parsedMessages: CliChatMessage[];
|
|
94
|
-
lastParsedAssistant: CliChatMessage | undefined;
|
|
95
|
-
parsedStatus: string | null;
|
|
96
|
-
prevStatus: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
88
|
interface SendMessageState {
|
|
100
89
|
text: string;
|
|
101
90
|
normalizedPromptSnippet: string;
|
|
@@ -130,26 +119,23 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
|
|
|
130
119
|
// ─── Adapter ────────────────────────────────────────
|
|
131
120
|
|
|
132
121
|
export class ProviderCliAdapter implements CliAdapter {
|
|
133
|
-
|
|
122
|
+
cliType: string;
|
|
134
123
|
readonly cliName: string;
|
|
135
124
|
public workingDir: string;
|
|
136
125
|
|
|
137
126
|
private provider: CliProviderModule;
|
|
138
127
|
private ptyProcess: PtyRuntimeTransport | null = null;
|
|
139
128
|
private transportFactory: PtyTransportFactory;
|
|
140
|
-
private currentStatus: CliSessionStatus['status'] = 'starting';
|
|
141
129
|
private onStatusChange: (() => void) | null = null;
|
|
142
130
|
|
|
131
|
+
// ─── State machine engine ─────────────────────────
|
|
132
|
+
readonly engine: CliStateEngine;
|
|
133
|
+
|
|
143
134
|
private responseBuffer = '';
|
|
144
135
|
private recentOutputBuffer = '';
|
|
145
|
-
private
|
|
146
|
-
private activeModal: { message: string; buttons: string[] } | null = null;
|
|
147
|
-
private parseErrorMessage: string | null = null;
|
|
136
|
+
private get parseErrorMessage(): string | null { return this.runner.parseErrorMessage; }
|
|
148
137
|
private providerSessionId: string | null = null;
|
|
149
|
-
private providerErrorMessage: string | null = null;
|
|
150
|
-
private providerErrorReason: string | null = null;
|
|
151
138
|
private responseTimeout: NodeJS.Timeout | null = null;
|
|
152
|
-
private idleTimeout: NodeJS.Timeout | null = null;
|
|
153
139
|
private ready = false;
|
|
154
140
|
private startupBuffer = '';
|
|
155
141
|
private startupParseGate = false;
|
|
@@ -175,44 +161,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
175
161
|
private serverConn: any = null;
|
|
176
162
|
private logBuffer: { message: string; level: string }[] = [];
|
|
177
163
|
|
|
178
|
-
// Approval cooldown
|
|
179
|
-
private lastApprovalResolvedAt: number = 0;
|
|
180
|
-
|
|
181
|
-
// Approval state machine
|
|
182
|
-
private approvalTransitionBuffer: string = '';
|
|
183
|
-
private approvalExitTimeout: NodeJS.Timeout | null = null;
|
|
184
|
-
private pendingScriptStatus: 'generating' | 'waiting_approval' | null = null;
|
|
185
|
-
private pendingScriptStatusSince = 0;
|
|
186
|
-
private pendingScriptStatusTimer: NodeJS.Timeout | null = null;
|
|
187
|
-
|
|
188
|
-
// Output settle debounce — fires after PTY output goes quiet
|
|
189
|
-
private settleTimer: NodeJS.Timeout | null = null;
|
|
190
|
-
private settledBuffer: string = '';
|
|
191
|
-
private submitPendingUntil = 0;
|
|
192
|
-
private responseSettleIgnoreUntil = 0;
|
|
193
|
-
private responseEpoch = 0;
|
|
194
|
-
private submitRetryTimer: NodeJS.Timeout | null = null;
|
|
195
|
-
private submitRetryUsed = false;
|
|
196
|
-
private submitRetryPromptSnippet = '';
|
|
197
|
-
private idleFinishCandidate: IdleFinishCandidate | null = null;
|
|
198
|
-
private finishRetryTimer: NodeJS.Timeout | null = null;
|
|
199
|
-
private finishRetryCount = 0;
|
|
200
164
|
private pendingOutboundQueue: PendingOutboundMessage[] = [];
|
|
201
165
|
private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
|
|
202
166
|
private pendingOutboundFlushInFlight = false;
|
|
203
|
-
|
|
204
|
-
private
|
|
167
|
+
// Submit retry timer — PTY-level, not state machine
|
|
168
|
+
private submitRetryTimer: NodeJS.Timeout | null = null;
|
|
205
169
|
|
|
206
170
|
// Resize redraw suppression
|
|
207
171
|
private resizeSuppressUntil: number = 0;
|
|
208
172
|
|
|
209
|
-
|
|
210
|
-
|
|
173
|
+
// Native transcript anchor — when >0, native history was confirmed for this session.
|
|
174
|
+
// Prevents freshEnough flips caused by PTY buffer activity after the first successful native read.
|
|
175
|
+
nativeHistoryAnchoredAt: number = 0;
|
|
211
176
|
|
|
212
|
-
// ───
|
|
213
|
-
private
|
|
214
|
-
/**
|
|
215
|
-
|
|
177
|
+
// ─── Script runner (parsing isolated here, adapter stays as transport) ───
|
|
178
|
+
private readonly runner: CliScriptRunner;
|
|
179
|
+
/** @deprecated use runner.cliScripts for direct script access */
|
|
180
|
+
get cliScripts(): CliScripts { return this.runner.cliScripts; }
|
|
181
|
+
set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
|
|
216
182
|
private runtimeSettings: Record<string, any> = {};
|
|
217
183
|
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
218
184
|
private accumulatedBuffer: string = '';
|
|
@@ -232,10 +198,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
232
198
|
* Hermes turn (tool calls + reasoning + final bubble) without the
|
|
233
199
|
* rolling window pushing the turn's ╭─ opening line out of view. */
|
|
234
200
|
private static readonly MAX_ACCUMULATED_BUFFER = 262144;
|
|
235
|
-
private currentTurnScope: TurnParseScope | null = null;
|
|
236
|
-
private traceEntries: CliTraceEntry[] = [];
|
|
237
|
-
private traceSeq = 0;
|
|
238
|
-
private traceSessionId = '';
|
|
239
201
|
private parsedStatusCache: {
|
|
240
202
|
responseBuffer: string;
|
|
241
203
|
currentTurnScope: TurnParseScope | null;
|
|
@@ -249,11 +211,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
249
211
|
result: any;
|
|
250
212
|
} | null = null;
|
|
251
213
|
private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
252
|
-
private static readonly MAX_TRACE_ENTRIES = 250;
|
|
253
214
|
|
|
254
215
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
255
|
-
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
256
|
-
private static readonly MAX_FINISH_RETRIES = 2;
|
|
257
216
|
|
|
258
217
|
private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
|
|
259
218
|
const build = (droppedChars: number, maxChars: number) => droppedChars > 0
|
|
@@ -329,13 +288,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
329
288
|
if (
|
|
330
289
|
cached
|
|
331
290
|
&& cached.responseBuffer === this.responseBuffer
|
|
332
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
291
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
333
292
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
334
293
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
335
294
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
336
295
|
&& cached.screenText === this.lastScreenText
|
|
337
|
-
&& cached.currentStatus === this.currentStatus
|
|
338
|
-
&& cached.activeModal === this.activeModal
|
|
296
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
297
|
+
&& cached.activeModal === this.engine.activeModal
|
|
339
298
|
&& cached.cliName === this.cliName
|
|
340
299
|
) {
|
|
341
300
|
return cached.result;
|
|
@@ -359,86 +318,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
359
318
|
return this.timeouts.statusActivityHold;
|
|
360
319
|
}
|
|
361
320
|
|
|
362
|
-
private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
|
|
363
|
-
const prev = this.currentStatus;
|
|
364
|
-
if (prev === status) return;
|
|
365
|
-
this.currentStatus = status;
|
|
366
|
-
this.statusHistory.push({ status, at: Date.now(), trigger });
|
|
367
|
-
if (this.statusHistory.length > 50) this.statusHistory.shift();
|
|
368
|
-
this.recordTrace('status', {
|
|
369
|
-
previousStatus: prev,
|
|
370
|
-
trigger: trigger || null,
|
|
371
|
-
});
|
|
372
|
-
LOG.info('CLI', `[${this.cliType}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
private clearIdleFinishCandidate(reason: string): void {
|
|
376
|
-
if (!this.idleFinishCandidate) return;
|
|
377
|
-
this.recordTrace('idle_candidate_reset', {
|
|
378
|
-
reason,
|
|
379
|
-
candidate: this.idleFinishCandidate,
|
|
380
|
-
});
|
|
381
|
-
this.idleFinishCandidate = null;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
private armIdleFinishCandidate(assistantLength: number): void {
|
|
385
|
-
const now = Date.now();
|
|
386
|
-
const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
|
|
387
|
-
this.idleFinishCandidate = {
|
|
388
|
-
armedAt: now,
|
|
389
|
-
lastOutputAt: this.lastOutputAt,
|
|
390
|
-
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
391
|
-
responseEpoch: this.responseEpoch,
|
|
392
|
-
assistantLength,
|
|
393
|
-
};
|
|
394
|
-
this.recordTrace('idle_candidate_armed', {
|
|
395
|
-
confirmMs: idleFinishConfirmMs,
|
|
396
|
-
candidate: this.idleFinishCandidate,
|
|
397
|
-
...buildCliTraceParseSnapshot({
|
|
398
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
399
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
400
|
-
responseBuffer: this.responseBuffer,
|
|
401
|
-
partialResponse: this.responseBuffer,
|
|
402
|
-
scope: this.currentTurnScope,
|
|
403
|
-
}),
|
|
404
|
-
});
|
|
405
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
406
|
-
this.settleTimer = setTimeout(() => {
|
|
407
|
-
this.settleTimer = null;
|
|
408
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
409
|
-
this.evaluateSettled();
|
|
410
|
-
}, idleFinishConfirmMs);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
private recordTrace(type: string, payload: Record<string, any> = {}): void {
|
|
415
|
-
const entry: CliTraceEntry = {
|
|
416
|
-
id: ++this.traceSeq,
|
|
417
|
-
at: Date.now(),
|
|
418
|
-
type,
|
|
419
|
-
status: this.currentStatus,
|
|
420
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
421
|
-
activeModal: this.activeModal
|
|
422
|
-
? { message: this.activeModal.message, buttons: [...this.activeModal.buttons] }
|
|
423
|
-
: null,
|
|
424
|
-
payload,
|
|
425
|
-
};
|
|
426
|
-
this.traceEntries.push(entry);
|
|
427
|
-
if (this.traceEntries.length > ProviderCliAdapter.MAX_TRACE_ENTRIES) {
|
|
428
|
-
this.traceEntries.splice(0, this.traceEntries.length - ProviderCliAdapter.MAX_TRACE_ENTRIES);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
private resetTraceSession(): void {
|
|
433
|
-
this.traceEntries = [];
|
|
434
|
-
this.traceSeq = 0;
|
|
435
|
-
this.traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
436
|
-
this.recordTrace('session_start', {
|
|
437
|
-
providerType: this.cliType,
|
|
438
|
-
workingDir: this.workingDir,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
321
|
// Resolved timeouts
|
|
443
322
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
444
323
|
|
|
@@ -448,7 +327,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
448
327
|
private readonly sendKey: string;
|
|
449
328
|
private readonly submitStrategy: 'wait_for_echo' | 'immediate';
|
|
450
329
|
private readonly requirePromptEchoBeforeSubmit: boolean;
|
|
451
|
-
private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
|
|
452
330
|
|
|
453
331
|
constructor(
|
|
454
332
|
provider: CliProviderModule,
|
|
@@ -457,6 +335,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
457
335
|
private extraEnv: Record<string, string> = {},
|
|
458
336
|
transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
|
|
459
337
|
) {
|
|
338
|
+
this.runner = new CliScriptRunner(provider.type);
|
|
460
339
|
this.provider = provider;
|
|
461
340
|
this.transportFactory = transportFactory;
|
|
462
341
|
this.cliType = provider.type;
|
|
@@ -474,10 +353,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
474
353
|
this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
|
|
475
354
|
this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
|
|
476
355
|
|
|
477
|
-
//
|
|
478
|
-
this.
|
|
479
|
-
|
|
480
|
-
|
|
356
|
+
// State machine engine — owns all status transitions
|
|
357
|
+
this.engine = new CliStateEngine(
|
|
358
|
+
provider,
|
|
359
|
+
this.runner,
|
|
360
|
+
this as unknown as CliTransportAccess,
|
|
361
|
+
{
|
|
362
|
+
onStatusChange: () => { this.onStatusChange?.(); },
|
|
363
|
+
onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
|
|
364
|
+
onTurnCompleted: () => { this.responseBuffer = ''; },
|
|
365
|
+
} satisfies CliStateEngineCallbacks,
|
|
366
|
+
resolvedConfig.timeouts,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Scripts delegated to CliScriptRunner — adapter stays as transport
|
|
370
|
+
this.runner.setScripts(provider.scripts || {});
|
|
371
|
+
const scriptNames = this.runner.getScriptNames();
|
|
481
372
|
if (scriptNames.length > 0) {
|
|
482
373
|
LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
|
|
483
374
|
LOG.info(
|
|
@@ -503,14 +394,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
503
394
|
|
|
504
395
|
/** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
|
|
505
396
|
setCliScripts(scripts: CliScripts): void {
|
|
506
|
-
this.
|
|
397
|
+
this.runner.setScripts(scripts);
|
|
507
398
|
this.parsedStatusCache = null;
|
|
508
|
-
this.
|
|
509
|
-
// Initialize per-session state: createState() is called once here and on script reload.
|
|
510
|
-
// The returned object lives until the PTY exits (scriptState = null on exit).
|
|
511
|
-
this.scriptState = typeof scripts.createState === 'function' ? (scripts.createState() ?? null) : null;
|
|
512
|
-
const scriptNames = listCliScriptNames(scripts);
|
|
513
|
-
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
|
|
399
|
+
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
|
|
514
400
|
}
|
|
515
401
|
|
|
516
402
|
/** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
|
|
@@ -564,15 +450,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
564
450
|
});
|
|
565
451
|
|
|
566
452
|
LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
|
|
567
|
-
this.resetTraceSession();
|
|
568
|
-
this.recordTrace('spawn', {
|
|
569
|
-
shellCommand: spawnPlan.shellCmd,
|
|
570
|
-
shellArgs: spawnPlan.shellArgs,
|
|
571
|
-
cwd: spawnPlan.ptyOptions.cwd,
|
|
572
|
-
cols: spawnPlan.ptyOptions.cols,
|
|
573
|
-
rows: spawnPlan.ptyOptions.rows,
|
|
574
|
-
providerResolution: this.providerResolutionMeta,
|
|
575
|
-
});
|
|
576
453
|
|
|
577
454
|
try {
|
|
578
455
|
this.ptyProcess = this.transportFactory.spawn(
|
|
@@ -636,13 +513,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
636
513
|
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
637
514
|
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
|
|
638
515
|
this.flushPendingOutputParse();
|
|
639
|
-
this.recordTrace('exit', { exitCode });
|
|
640
516
|
this.ptyProcess = null;
|
|
641
|
-
this.
|
|
517
|
+
this.engine.onPtyExit();
|
|
642
518
|
this.ready = false;
|
|
643
519
|
this.startupParseGate = false;
|
|
644
520
|
this.spawnAt = 0;
|
|
645
|
-
this.
|
|
521
|
+
this.runner.resetSessionState();
|
|
646
522
|
this.onStatusChange?.();
|
|
647
523
|
});
|
|
648
524
|
|
|
@@ -653,15 +529,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
653
529
|
if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
|
|
654
530
|
this.resetTerminalScreen(24, 80);
|
|
655
531
|
this.pendingTerminalQueryTail = '';
|
|
656
|
-
this.currentTurnScope = null;
|
|
657
|
-
this.finishRetryCount = 0;
|
|
658
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
659
532
|
this.ready = false;
|
|
660
533
|
await this.ptyProcess.ready;
|
|
661
|
-
this.
|
|
662
|
-
runtimeMeta: this.getRuntimeMetadata(),
|
|
663
|
-
});
|
|
664
|
-
this.setStatus('starting', 'pty_ready');
|
|
534
|
+
this.engine.onSpawnReady();
|
|
665
535
|
this.scheduleStartupSettleCheck();
|
|
666
536
|
this.onStatusChange?.();
|
|
667
537
|
}
|
|
@@ -687,11 +557,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
687
557
|
if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
|
|
688
558
|
this.startupFirstOutputAt = now;
|
|
689
559
|
}
|
|
690
|
-
if (
|
|
691
|
-
this.clearIdleFinishCandidate('new_output');
|
|
560
|
+
if (rawData.length > 0 || cleanData.length > 0) {
|
|
561
|
+
this.engine.clearIdleFinishCandidate('new_output');
|
|
692
562
|
}
|
|
693
563
|
if (getDebugRuntimeConfig().collectDebugTrace) {
|
|
694
|
-
this.
|
|
564
|
+
this.engine.recordExternalTrace('output', {
|
|
695
565
|
rawLength: rawData.length,
|
|
696
566
|
cleanLength: cleanData.length,
|
|
697
567
|
rawPreview: summarizeCliTraceText(rawData, 300),
|
|
@@ -703,7 +573,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
703
573
|
this.scheduleStartupSettleCheck();
|
|
704
574
|
}
|
|
705
575
|
|
|
706
|
-
if (this.isWaitingForResponse && cleanData) {
|
|
576
|
+
if (this.engine.isWaitingForResponse && cleanData) {
|
|
707
577
|
const previousResponseLen = this.responseBuffer.length;
|
|
708
578
|
this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
|
|
709
579
|
this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
|
|
@@ -742,19 +612,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
742
612
|
// Keep turn-scope offsets aligned with the truncated buffer so scoped
|
|
743
613
|
// parses don't lose the beginning of a long turn (e.g. the Hermes
|
|
744
614
|
// ╭─ opening line) when the rolling window sheds bytes.
|
|
745
|
-
if (this.currentTurnScope) {
|
|
615
|
+
if (this.engine.currentTurnScope) {
|
|
746
616
|
if (droppedClean > 0) {
|
|
747
|
-
this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
|
|
617
|
+
this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
|
|
748
618
|
}
|
|
749
619
|
if (droppedRaw > 0) {
|
|
750
|
-
this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
|
|
620
|
+
this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
|
|
751
621
|
}
|
|
752
622
|
}
|
|
753
623
|
|
|
754
624
|
this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
|
|
755
625
|
|
|
756
626
|
// ─── Script-based status detection
|
|
757
|
-
this.scheduleSettle();
|
|
627
|
+
this.engine.scheduleSettle();
|
|
758
628
|
}
|
|
759
629
|
|
|
760
630
|
private resolveStartupState(
|
|
@@ -779,12 +649,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
779
649
|
const startupModal = this.runParseApproval(this.recentOutputBuffer);
|
|
780
650
|
const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
781
651
|
if (!startupModal && startupStatus !== 'idle') {
|
|
782
|
-
this.recordTrace('startup_settle_deferred', {
|
|
783
|
-
trigger,
|
|
784
|
-
startupStatus,
|
|
785
|
-
stableMs,
|
|
786
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
787
|
-
});
|
|
788
652
|
this.scheduleStartupSettleCheck();
|
|
789
653
|
return;
|
|
790
654
|
}
|
|
@@ -795,14 +659,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
795
659
|
}
|
|
796
660
|
this.ready = true;
|
|
797
661
|
if (startupModal) {
|
|
798
|
-
this.activeModal = startupModal;
|
|
799
|
-
this.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
662
|
+
this.engine.activeModal = startupModal;
|
|
663
|
+
this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
800
664
|
} else {
|
|
801
|
-
if (this.currentStatus === 'waiting_approval' || this.activeModal) {
|
|
802
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
665
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
|
|
666
|
+
this.engine.lastApprovalResolvedAt = Date.now();
|
|
803
667
|
}
|
|
804
|
-
this.activeModal = null;
|
|
805
|
-
this.setStatus('idle', `startup_ready:${trigger}`);
|
|
668
|
+
this.engine.activeModal = null;
|
|
669
|
+
this.engine.setStatus('idle', `startup_ready:${trigger}`);
|
|
806
670
|
}
|
|
807
671
|
LOG.info(
|
|
808
672
|
'CLI',
|
|
@@ -828,88 +692,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
828
692
|
}, delayMs);
|
|
829
693
|
}
|
|
830
694
|
|
|
831
|
-
private scheduleSettle(): void {
|
|
832
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
833
|
-
const settleEpoch = this.responseEpoch;
|
|
834
|
-
const delay = Math.max(
|
|
835
|
-
this.timeouts.outputSettle,
|
|
836
|
-
this.submitPendingUntil > Date.now()
|
|
837
|
-
? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
|
|
838
|
-
: 0,
|
|
839
|
-
);
|
|
840
|
-
this.settleTimer = setTimeout(() => {
|
|
841
|
-
this.settleTimer = null;
|
|
842
|
-
if (settleEpoch !== this.responseEpoch) return;
|
|
843
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
844
|
-
this.evaluateSettled();
|
|
845
|
-
}, delay);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
private armApprovalExitTimeout(): void {
|
|
849
|
-
if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
|
|
850
|
-
this.approvalExitTimeout = setTimeout(() => {
|
|
851
|
-
if (!this.hasActionableApproval()) return;
|
|
852
|
-
const tail = this.recentOutputBuffer;
|
|
853
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
854
|
-
const modal = this.runParseApproval(tail);
|
|
855
|
-
const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
|
|
856
|
-
if (stillWaiting) {
|
|
857
|
-
if (!modal) {
|
|
858
|
-
LOG.warn('CLI', `[${this.cliType}] approval timeout check found no actionable modal; keeping approval state fail-closed`);
|
|
859
|
-
this.activeModal = null;
|
|
860
|
-
this.onStatusChange?.();
|
|
861
|
-
this.armApprovalExitTimeout();
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
this.activeModal = modal;
|
|
865
|
-
this.onStatusChange?.();
|
|
866
|
-
this.armApprovalExitTimeout();
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
|
|
870
|
-
this.activeModal = null;
|
|
871
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
872
|
-
this.setStatus('idle', 'approval_timeout');
|
|
873
|
-
this.onStatusChange?.();
|
|
874
|
-
}, 60000);
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
private shouldRetryFinishResponse(commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
|
|
878
|
-
if (!this.currentTurnScope) return false;
|
|
879
|
-
if (this.hasActionableApproval()) return false;
|
|
880
|
-
if (this.finishRetryCount >= ProviderCliAdapter.MAX_FINISH_RETRIES) return false;
|
|
881
|
-
if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
|
|
882
|
-
|
|
883
|
-
if (this.runDetectStatus(this.recentOutputBuffer) !== 'idle') return false;
|
|
884
|
-
|
|
885
|
-
const now = Date.now();
|
|
886
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
887
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
888
|
-
return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
private hasRecentInteractiveActivity(now: number): boolean {
|
|
892
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
893
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : Number.MAX_SAFE_INTEGER;
|
|
894
|
-
const holdMs = this.getStatusActivityHoldMs();
|
|
895
|
-
return quietForMs < holdMs
|
|
896
|
-
|| screenStableMs < holdMs;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
private shouldDeferIdleTimeoutFinish(): boolean {
|
|
900
|
-
if (!this.isWaitingForResponse || this.hasActionableApproval()) {
|
|
901
|
-
return false;
|
|
902
|
-
}
|
|
903
|
-
const latestStatus = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
904
|
-
if (latestStatus === 'generating') {
|
|
905
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
906
|
-
this.evaluateSettled();
|
|
907
|
-
return true;
|
|
908
|
-
}
|
|
909
|
-
return false;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
|
|
913
695
|
private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
|
|
914
696
|
const startedAt = Date.now();
|
|
915
697
|
let loggedWait = false;
|
|
@@ -919,7 +701,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
919
701
|
const screenText = this.terminalScreen.getText() || '';
|
|
920
702
|
const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
|
|
921
703
|
const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
922
|
-
const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
704
|
+
const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
|
|
923
705
|
const interactiveReady = status === 'idle'
|
|
924
706
|
&& stableMs >= 700
|
|
925
707
|
&& recentlyOutput >= 350;
|
|
@@ -953,795 +735,66 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
953
735
|
|
|
954
736
|
private clearAllTimers(): void {
|
|
955
737
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
956
|
-
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
957
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
958
738
|
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
959
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
960
|
-
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
961
|
-
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
962
739
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
963
740
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
964
|
-
|
|
965
|
-
this.providerErrorRetryKey = '';
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
private clearStaleIdleResponseGuard(reason: string): boolean {
|
|
969
|
-
const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
|
|
970
|
-
const isIdle = this.runDetectStatus(this.recentOutputBuffer) === 'idle';
|
|
971
|
-
if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !isIdle || !!blockingModal) {
|
|
972
|
-
return false;
|
|
973
|
-
}
|
|
974
|
-
this.clearAllTimers();
|
|
975
|
-
this.clearIdleFinishCandidate(reason);
|
|
976
|
-
this.responseBuffer = '';
|
|
977
|
-
this.isWaitingForResponse = false;
|
|
978
|
-
this.responseSettleIgnoreUntil = 0;
|
|
979
|
-
this.submitRetryUsed = false;
|
|
980
|
-
this.submitRetryPromptSnippet = '';
|
|
981
|
-
this.finishRetryCount = 0;
|
|
982
|
-
this.currentTurnScope = null;
|
|
983
|
-
this.activeModal = null;
|
|
984
|
-
this.recordTrace('stale_idle_response_cleared', { reason });
|
|
985
|
-
return true;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
private clearParsedIdleResponseGuard(reason: string, parsedStatus: any): boolean {
|
|
989
|
-
const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
|
|
990
|
-
const parsedModal = parsedStatus?.activeModal ?? parsedStatus?.modal ?? null;
|
|
991
|
-
const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
|
|
992
|
-
if (
|
|
993
|
-
!this.isWaitingForResponse
|
|
994
|
-
|| parsedRawStatus !== 'idle'
|
|
995
|
-
|| !!parsedModal
|
|
996
|
-
|| !!blockingModal
|
|
997
|
-
|| !this.parsedStatusHasFinalAssistantMessage(parsedStatus)
|
|
998
|
-
) {
|
|
999
|
-
return false;
|
|
1000
|
-
}
|
|
1001
|
-
this.clearAllTimers();
|
|
1002
|
-
this.clearIdleFinishCandidate(reason);
|
|
1003
|
-
this.responseBuffer = '';
|
|
1004
|
-
this.isWaitingForResponse = false;
|
|
1005
|
-
this.responseSettleIgnoreUntil = 0;
|
|
1006
|
-
this.submitRetryUsed = false;
|
|
1007
|
-
this.submitRetryPromptSnippet = '';
|
|
1008
|
-
this.finishRetryCount = 0;
|
|
1009
|
-
this.currentTurnScope = null;
|
|
1010
|
-
this.activeModal = null;
|
|
1011
|
-
this.setStatus('idle', reason);
|
|
1012
|
-
this.recordTrace('parsed_idle_response_cleared', {
|
|
1013
|
-
reason,
|
|
1014
|
-
parsedStatus: parsedRawStatus,
|
|
1015
|
-
parsedMessageCount: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : 0,
|
|
1016
|
-
});
|
|
1017
|
-
return true;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
|
|
1021
|
-
const raw = String(this.responseBuffer || '').trim();
|
|
1022
|
-
if (!raw) return false;
|
|
1023
|
-
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1024
|
-
if (!normalizedPrompt) return true;
|
|
1025
|
-
const normalizedBuffer = compactPromptText(raw);
|
|
1026
|
-
if (!normalizedBuffer) return false;
|
|
1027
|
-
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1028
|
-
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1029
|
-
const remainder = normalizedBuffer
|
|
1030
|
-
.slice(normalizedPrompt.length)
|
|
1031
|
-
.replace(/[─═\-]+/g, '')
|
|
1032
|
-
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1033
|
-
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1034
|
-
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1035
|
-
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1036
|
-
.replace(/esctointerrupt/gi, '')
|
|
1037
|
-
.replace(/❯/g, '')
|
|
1038
|
-
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1039
|
-
.trim();
|
|
1040
|
-
return remainder.length > 0;
|
|
1041
|
-
}
|
|
1042
|
-
return true;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
private evaluateSettled(): void {
|
|
1046
|
-
const now = Date.now();
|
|
1047
|
-
if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
|
|
1048
|
-
const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
|
|
1049
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
1050
|
-
this.settleTimer = setTimeout(() => {
|
|
1051
|
-
this.settleTimer = null;
|
|
1052
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1053
|
-
this.evaluateSettled();
|
|
1054
|
-
}, delayTime);
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
this.resolveStartupState('settled');
|
|
1059
|
-
if (this.startupParseGate) return;
|
|
1060
|
-
|
|
1061
|
-
if (!this.isWaitingForResponse && !this.currentTurnScope && !this.activeModal && !this.parseErrorMessage) {
|
|
1062
|
-
const tail = this.settledBuffer || this.recentOutputBuffer;
|
|
1063
|
-
const modal = this.runParseApproval(tail);
|
|
1064
|
-
const lightweightStatus = this.cliScripts?.detectStatus
|
|
1065
|
-
? this.runDetectStatus(tail)
|
|
1066
|
-
: null;
|
|
1067
|
-
if (!modal && lightweightStatus === 'idle' && this.currentStatus === 'idle') {
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
const session = this.runParseSession();
|
|
1073
|
-
if (!session) return;
|
|
1074
|
-
|
|
1075
|
-
const { status, messages, parsedStatus } = session;
|
|
1076
|
-
const modal = (session as any).activeModal ?? session.modal ?? null;
|
|
1077
|
-
const parsedMessages = normalizeCliParsedMessages(messages, {
|
|
1078
|
-
scope: null,
|
|
1079
|
-
lastOutputAt: this.lastOutputAt,
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages)) return;
|
|
1083
|
-
|
|
1084
|
-
const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
|
|
1085
|
-
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || '');
|
|
1086
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1087
|
-
|
|
1088
|
-
this.recordTrace('settled', {
|
|
1089
|
-
tail: summarizeCliTraceText(this.settledBuffer, 500),
|
|
1090
|
-
screenText: summarizeCliTraceText(screenText, 1200),
|
|
1091
|
-
detectStatus: status,
|
|
1092
|
-
parsedStatus: parsedStatus || null,
|
|
1093
|
-
parsedMessageCount: parsedMessages.length,
|
|
1094
|
-
parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
|
|
1095
|
-
parsedActiveModal: modal,
|
|
1096
|
-
approval: modal,
|
|
1097
|
-
...buildCliTraceParseSnapshot({
|
|
1098
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1099
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1100
|
-
responseBuffer: this.responseBuffer,
|
|
1101
|
-
partialResponse: this.responseBuffer,
|
|
1102
|
-
scope: this.currentTurnScope,
|
|
1103
|
-
}),
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
if (
|
|
1107
|
-
this.currentTurnScope
|
|
1108
|
-
&& !lastParsedAssistant
|
|
1109
|
-
&& !this.submitRetryUsed
|
|
1110
|
-
&& this.ptyProcess
|
|
1111
|
-
&& !this.hasActionableApproval()
|
|
1112
|
-
&& promptLikelyVisible(screenText, normalizedPromptSnippet)
|
|
1113
|
-
&& !this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)
|
|
1114
|
-
) {
|
|
1115
|
-
this.submitRetryUsed = true;
|
|
1116
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1117
|
-
LOG.info('CLI', `[${this.cliType}] Retrying submit key from settled parser (no assistant yet)`);
|
|
1118
|
-
this.recordTrace('submit_write', {
|
|
1119
|
-
mode: 'settled_retry',
|
|
1120
|
-
sendKey: this.sendKey,
|
|
1121
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
1122
|
-
});
|
|
1123
|
-
this.ptyProcess.write(this.sendKey);
|
|
1124
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
1125
|
-
this.settleTimer = setTimeout(() => {
|
|
1126
|
-
this.settleTimer = null;
|
|
1127
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1128
|
-
this.evaluateSettled();
|
|
1129
|
-
}, this.timeouts.outputSettle + 150);
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (this.currentTurnScope && !lastParsedAssistant) {
|
|
1134
|
-
LOG.debug(
|
|
1135
|
-
'CLI',
|
|
1136
|
-
`[${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 || '-'}`
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if (!status) return;
|
|
1141
|
-
|
|
1142
|
-
const prevStatus = this.currentStatus;
|
|
1143
|
-
const ctx: SettledEvalContext = { now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus };
|
|
1144
|
-
|
|
1145
|
-
if (!this.applyPendingScriptStatusDebounce(ctx)) return;
|
|
1146
|
-
|
|
1147
|
-
const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
|
|
1148
|
-
LOG.debug(
|
|
1149
|
-
'CLI',
|
|
1150
|
-
`[${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)}`
|
|
1151
|
-
);
|
|
1152
|
-
|
|
1153
|
-
const shouldHoldGenerating =
|
|
1154
|
-
status === 'idle'
|
|
1155
|
-
&& this.isWaitingForResponse
|
|
1156
|
-
&& !modal
|
|
1157
|
-
&& recentInteractiveActivity
|
|
1158
|
-
&& !(parsedStatus === 'idle' && !!lastParsedAssistant);
|
|
1159
|
-
|
|
1160
|
-
if (shouldHoldGenerating) { this.applyHoldGenerating(ctx, recentInteractiveActivity); return; }
|
|
1161
|
-
if (status === 'error') {
|
|
1162
|
-
if (this.maybeScheduleProviderErrorRetry(ctx, session)) return;
|
|
1163
|
-
this.applyError(ctx, session);
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
|
|
1167
|
-
if (status === 'generating') { this.applyGenerating(ctx); return; }
|
|
1168
|
-
if (status === 'idle') { this.applyIdle(ctx, now); }
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
// Returns false if the caller should bail out (debounce pending).
|
|
1172
|
-
private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
|
|
1173
|
-
const { now, status, prevStatus } = ctx;
|
|
1174
|
-
const shouldDebounce =
|
|
1175
|
-
prevStatus === 'idle'
|
|
1176
|
-
&& !this.isWaitingForResponse
|
|
1177
|
-
&& !this.currentTurnScope
|
|
1178
|
-
&& (status === 'generating' || status === 'waiting_approval');
|
|
1179
|
-
|
|
1180
|
-
if (!shouldDebounce) {
|
|
1181
|
-
this.pendingScriptStatus = null;
|
|
1182
|
-
this.pendingScriptStatusSince = 0;
|
|
1183
|
-
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
1184
|
-
return true;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const armPending = (delayMs: number) => {
|
|
1188
|
-
if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
|
|
1189
|
-
this.pendingScriptStatusTimer = setTimeout(() => {
|
|
1190
|
-
this.pendingScriptStatusTimer = null;
|
|
1191
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1192
|
-
this.evaluateSettled();
|
|
1193
|
-
}, delayMs);
|
|
1194
|
-
};
|
|
1195
|
-
|
|
1196
|
-
if (this.pendingScriptStatus !== status) {
|
|
1197
|
-
this.pendingScriptStatus = status as 'generating' | 'waiting_approval';
|
|
1198
|
-
this.pendingScriptStatusSince = now;
|
|
1199
|
-
armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
|
|
1200
|
-
return false;
|
|
1201
|
-
}
|
|
1202
|
-
const elapsed = now - this.pendingScriptStatusSince;
|
|
1203
|
-
if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
|
|
1204
|
-
armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
|
|
1205
|
-
return false;
|
|
1206
|
-
}
|
|
1207
|
-
return true;
|
|
741
|
+
this.engine.clearAllTimers();
|
|
1208
742
|
}
|
|
1209
743
|
|
|
1210
|
-
|
|
1211
|
-
const { status } = ctx;
|
|
1212
|
-
this.clearIdleFinishCandidate('hold_generating_recent_activity');
|
|
1213
|
-
this.setStatus('generating', 'recent_activity_hold');
|
|
1214
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1215
|
-
this.idleTimeout = setTimeout(() => {
|
|
1216
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1217
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1218
|
-
this.finishResponse();
|
|
1219
|
-
}
|
|
1220
|
-
}, this.timeouts.generatingIdle);
|
|
1221
|
-
this.recordTrace('hold_generating_recent_activity', {
|
|
1222
|
-
scriptStatus: status,
|
|
1223
|
-
recentInteractiveActivity,
|
|
1224
|
-
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
1225
|
-
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1226
|
-
holdMs: this.getStatusActivityHoldMs(),
|
|
1227
|
-
...buildCliTraceParseSnapshot({
|
|
1228
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1229
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1230
|
-
responseBuffer: this.responseBuffer,
|
|
1231
|
-
partialResponse: this.responseBuffer,
|
|
1232
|
-
scope: this.currentTurnScope,
|
|
1233
|
-
}),
|
|
1234
|
-
});
|
|
1235
|
-
this.onStatusChange?.();
|
|
1236
|
-
}
|
|
744
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
1237
745
|
|
|
1238
|
-
|
|
1239
|
-
const
|
|
1240
|
-
this.
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
this.
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
|
|
1256
|
-
}
|
|
1257
|
-
this.onStatusChange?.();
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
if (!inCooldown) {
|
|
1261
|
-
if (!modal) {
|
|
1262
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
this.isWaitingForResponse = true;
|
|
1266
|
-
this.setStatus('waiting_approval', 'script_detect');
|
|
1267
|
-
this.activeModal = modal;
|
|
1268
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1269
|
-
this.armApprovalExitTimeout();
|
|
1270
|
-
this.onStatusChange?.();
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
private applyGenerating(ctx: SettledEvalContext): void {
|
|
1275
|
-
const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
|
|
1276
|
-
this.clearIdleFinishCandidate('generating');
|
|
1277
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1278
|
-
const effectiveScreenText = screenText || this.accumulatedBuffer;
|
|
1279
|
-
const noActiveTurn = !this.currentTurnScope;
|
|
1280
|
-
const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
|
|
1281
|
-
const parsedShowsLiveAssistantProgress = parsedStatus === 'generating'
|
|
1282
|
-
&& !!lastParsedAssistant
|
|
1283
|
-
;
|
|
1284
|
-
if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
if (prevStatus === 'waiting_approval') {
|
|
1288
|
-
// Transitioned out of approval → generating
|
|
1289
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1290
|
-
this.activeModal = null;
|
|
1291
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1292
|
-
}
|
|
1293
|
-
if (!this.isWaitingForResponse) {
|
|
1294
|
-
this.isWaitingForResponse = true;
|
|
1295
|
-
this.responseBuffer = '';
|
|
1296
|
-
}
|
|
1297
|
-
this.setStatus('generating', 'script_detect');
|
|
1298
|
-
// Reset idle timeout
|
|
1299
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1300
|
-
this.idleTimeout = setTimeout(() => {
|
|
1301
|
-
if (this.isWaitingForResponse) {
|
|
1302
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1303
|
-
this.finishResponse();
|
|
1304
|
-
}
|
|
1305
|
-
}, this.timeouts.generatingIdle);
|
|
1306
|
-
this.onStatusChange?.();
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
private applyError(ctx: SettledEvalContext, session: ParsedSession): void {
|
|
1310
|
-
this.clearIdleFinishCandidate('provider_error');
|
|
1311
|
-
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1312
|
-
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
1313
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1314
|
-
this.isWaitingForResponse = false;
|
|
1315
|
-
this.responseSettleIgnoreUntil = 0;
|
|
1316
|
-
this.submitRetryUsed = false;
|
|
1317
|
-
this.submitRetryPromptSnippet = '';
|
|
1318
|
-
this.finishRetryCount = 0;
|
|
1319
|
-
this.currentTurnScope = null;
|
|
1320
|
-
this.activeModal = null;
|
|
1321
|
-
this.providerErrorMessage = typeof session.errorMessage === 'string' && session.errorMessage.trim()
|
|
1322
|
-
? session.errorMessage.trim()
|
|
1323
|
-
: 'Provider reported an error';
|
|
1324
|
-
this.providerErrorReason = typeof session.errorReason === 'string' && session.errorReason.trim()
|
|
1325
|
-
? session.errorReason.trim()
|
|
1326
|
-
: 'provider_error';
|
|
1327
|
-
this.setStatus('error', this.providerErrorReason);
|
|
1328
|
-
this.recordTrace('provider_error', {
|
|
1329
|
-
errorMessage: this.providerErrorMessage,
|
|
1330
|
-
errorReason: this.providerErrorReason,
|
|
1331
|
-
parsedStatus: ctx.parsedStatus || ctx.status,
|
|
1332
|
-
messageCount: ctx.parsedMessages.length,
|
|
1333
|
-
...buildCliTraceParseSnapshot({
|
|
1334
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1335
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1336
|
-
responseBuffer: this.responseBuffer,
|
|
1337
|
-
partialResponse: this.responseBuffer,
|
|
1338
|
-
scope: this.currentTurnScope,
|
|
1339
|
-
}),
|
|
1340
|
-
});
|
|
1341
|
-
this.onStatusChange?.();
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
private maybeScheduleProviderErrorRetry(ctx: SettledEvalContext, session: ParsedSession): boolean {
|
|
1345
|
-
const retryPrompt = typeof (session as any).retryPrompt === 'string'
|
|
1346
|
-
? String((session as any).retryPrompt).trim()
|
|
1347
|
-
: '';
|
|
1348
|
-
const retryDelayMs = typeof (session as any).retryDelayMs === 'number'
|
|
1349
|
-
? Number((session as any).retryDelayMs)
|
|
1350
|
-
: NaN;
|
|
1351
|
-
if (!retryPrompt || !Number.isFinite(retryDelayMs) || retryDelayMs < 0) return false;
|
|
1352
|
-
if (!this.ptyProcess) return false;
|
|
1353
|
-
|
|
1354
|
-
const retryAttempt = typeof (session as any).retryAttempt === 'number'
|
|
1355
|
-
? Number((session as any).retryAttempt)
|
|
1356
|
-
: 0;
|
|
1357
|
-
const retryMaxAttempts = typeof (session as any).retryMaxAttempts === 'number'
|
|
1358
|
-
? Number((session as any).retryMaxAttempts)
|
|
1359
|
-
: 0;
|
|
1360
|
-
const errorReason = typeof session.errorReason === 'string' && session.errorReason.trim()
|
|
1361
|
-
? session.errorReason.trim()
|
|
1362
|
-
: 'provider_error';
|
|
1363
|
-
const retryKey = `${errorReason}:${retryAttempt}:${retryPrompt}`;
|
|
1364
|
-
if (this.providerErrorRetryTimer && this.providerErrorRetryKey === retryKey) return true;
|
|
1365
|
-
|
|
1366
|
-
if (this.providerErrorRetryTimer) clearTimeout(this.providerErrorRetryTimer);
|
|
1367
|
-
this.providerErrorRetryKey = retryKey;
|
|
1368
|
-
this.clearIdleFinishCandidate('provider_error_retry');
|
|
1369
|
-
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
1370
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1371
|
-
this.providerErrorMessage = typeof session.errorMessage === 'string' && session.errorMessage.trim()
|
|
1372
|
-
? session.errorMessage.trim()
|
|
1373
|
-
: 'Provider reported an error';
|
|
1374
|
-
this.providerErrorReason = errorReason;
|
|
1375
|
-
this.activeModal = null;
|
|
1376
|
-
this.responseSettleIgnoreUntil = Date.now() + retryDelayMs + this.timeouts.outputSettle + 400;
|
|
1377
|
-
this.setStatus('generating', 'provider_error_retry_scheduled');
|
|
1378
|
-
this.recordTrace('provider_error_retry_scheduled', {
|
|
1379
|
-
retryPrompt,
|
|
1380
|
-
retryDelayMs,
|
|
1381
|
-
retryAttempt,
|
|
1382
|
-
retryMaxAttempts,
|
|
1383
|
-
errorReason,
|
|
1384
|
-
parsedStatus: ctx.parsedStatus || ctx.status,
|
|
1385
|
-
});
|
|
1386
|
-
this.onStatusChange?.();
|
|
1387
|
-
this.providerErrorRetryTimer = setTimeout(() => {
|
|
1388
|
-
this.providerErrorRetryTimer = null;
|
|
1389
|
-
this.providerErrorRetryKey = '';
|
|
1390
|
-
if (!this.ptyProcess) return;
|
|
1391
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1392
|
-
this.submitRetryUsed = false;
|
|
1393
|
-
this.recordTrace('provider_error_retry_write', {
|
|
1394
|
-
retryPrompt,
|
|
1395
|
-
retryAttempt,
|
|
1396
|
-
retryMaxAttempts,
|
|
1397
|
-
errorReason,
|
|
1398
|
-
});
|
|
1399
|
-
this.ptyProcess.write(`${retryPrompt}\r`);
|
|
1400
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
1401
|
-
this.settleTimer = setTimeout(() => {
|
|
1402
|
-
this.settleTimer = null;
|
|
1403
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1404
|
-
this.evaluateSettled();
|
|
1405
|
-
}, this.timeouts.outputSettle + 150);
|
|
1406
|
-
}, retryDelayMs);
|
|
1407
|
-
return true;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
private applyIdle(ctx: SettledEvalContext, now: number): void {
|
|
1411
|
-
const { modal, lastParsedAssistant, prevStatus } = ctx;
|
|
1412
|
-
if (prevStatus === 'waiting_approval') {
|
|
1413
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1414
|
-
this.activeModal = null;
|
|
1415
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1416
|
-
this.setStatus('idle', 'approval_prompt_gone_script_idle');
|
|
1417
|
-
}
|
|
1418
|
-
if (!this.isWaitingForResponse) {
|
|
1419
|
-
if (prevStatus !== 'idle') {
|
|
1420
|
-
this.clearIdleFinishCandidate('idle_without_response');
|
|
1421
|
-
this.setStatus('idle', 'script_detect');
|
|
1422
|
-
this.onStatusChange?.();
|
|
1423
|
-
}
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
1427
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
1428
|
-
const hasAssistantTurn = !!lastParsedAssistant;
|
|
1429
|
-
const assistantLength = lastParsedAssistant?.content?.length || 0;
|
|
1430
|
-
const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
|
|
1431
|
-
const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
|
|
1432
|
-
const idleReady = !modal
|
|
1433
|
-
&& hasAssistantTurn
|
|
1434
|
-
&& quietForMs >= idleQuietThresholdMs
|
|
1435
|
-
&& screenStableMs >= idleFinishConfirmMs;
|
|
1436
|
-
const candidate = this.idleFinishCandidate;
|
|
1437
|
-
const candidateQuiet = !!candidate
|
|
1438
|
-
&& candidate.responseEpoch === this.responseEpoch
|
|
1439
|
-
&& candidate.lastOutputAt === this.lastOutputAt
|
|
1440
|
-
&& candidate.lastScreenChangeAt === this.lastScreenChangeAt
|
|
1441
|
-
&& assistantLength >= candidate.assistantLength
|
|
1442
|
-
&& (now - candidate.armedAt) >= idleFinishConfirmMs;
|
|
1443
|
-
|
|
1444
|
-
this.recordTrace('idle_decision', {
|
|
1445
|
-
quietForMs,
|
|
1446
|
-
screenStableMs,
|
|
1447
|
-
hasAssistantTurn,
|
|
1448
|
-
assistantLength,
|
|
1449
|
-
hasModal: !!modal,
|
|
1450
|
-
idleQuietThresholdMs,
|
|
1451
|
-
idleStableThresholdMs: idleFinishConfirmMs,
|
|
1452
|
-
idleReady,
|
|
1453
|
-
idleFinishConfirmMs,
|
|
1454
|
-
idleFinishCandidate: candidate,
|
|
1455
|
-
candidateQuiet,
|
|
1456
|
-
canFinishImmediately: idleReady && candidateQuiet,
|
|
1457
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
1458
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1459
|
-
...buildCliTraceParseSnapshot({
|
|
1460
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1461
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1462
|
-
responseBuffer: this.responseBuffer,
|
|
1463
|
-
partialResponse: this.responseBuffer,
|
|
1464
|
-
scope: this.currentTurnScope,
|
|
1465
|
-
}),
|
|
746
|
+
runParseSession(): ParsedSession | null {
|
|
747
|
+
const screenText = this.terminalScreen.getText();
|
|
748
|
+
const parseScreenText = this.getParseScreenText(screenText);
|
|
749
|
+
const tail = this.recentOutputBuffer.slice(-500);
|
|
750
|
+
const input = buildCliParseInput({
|
|
751
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
752
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
753
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
754
|
+
terminalScreenText: parseScreenText,
|
|
755
|
+
workingDir: this.workingDir,
|
|
756
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
757
|
+
historySessionId: this.providerSessionId || undefined,
|
|
758
|
+
baseMessages: [],
|
|
759
|
+
partialResponse: this.responseBuffer,
|
|
760
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
761
|
+
scope: this.engine.currentTurnScope,
|
|
762
|
+
runtimeSettings: this.runtimeSettings,
|
|
1466
763
|
});
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
this.finishResponse();
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
if (idleReady) {
|
|
1476
|
-
if (!candidate) {
|
|
1477
|
-
this.armIdleFinishCandidate(assistantLength);
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
} else {
|
|
1481
|
-
this.clearIdleFinishCandidate('idle_not_ready');
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1485
|
-
this.idleTimeout = setTimeout(() => {
|
|
1486
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1487
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1488
|
-
const parsed = this.runParseSession();
|
|
1489
|
-
if (this.shouldKeepCodexTurnOpenForFinish(parsed)) {
|
|
1490
|
-
this.rescheduleCodexFinishCheck('codex_idle_timeout_not_final');
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
this.clearIdleFinishCandidate('idle_timeout_finish');
|
|
1494
|
-
this.finishResponse();
|
|
1495
|
-
}
|
|
1496
|
-
}, this.timeouts.idleFinish);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
private finishResponse(): void {
|
|
1500
|
-
if (this.submitPendingUntil > Date.now()) return;
|
|
1501
|
-
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
1502
|
-
const parsedBeforeFinish = this.runParseSession();
|
|
1503
|
-
if (this.shouldKeepCodexTurnOpenForFinish(parsedBeforeFinish)) {
|
|
1504
|
-
this.rescheduleCodexFinishCheck('codex_finish_not_final');
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
this.clearIdleFinishCandidate('finish_response_enter');
|
|
1508
|
-
this.recordTrace('finish_response', {
|
|
1509
|
-
...buildCliTraceParseSnapshot({
|
|
1510
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1511
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1512
|
-
responseBuffer: this.responseBuffer,
|
|
1513
|
-
partialResponse: this.responseBuffer,
|
|
1514
|
-
scope: this.currentTurnScope,
|
|
1515
|
-
}),
|
|
764
|
+
const session = this.runner.parseSession({
|
|
765
|
+
...input,
|
|
766
|
+
tail,
|
|
767
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1516
768
|
});
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
this.finishRetryCount += 1;
|
|
1520
|
-
this.recordTrace('finish_response_retry', {
|
|
1521
|
-
retryCount: this.finishRetryCount,
|
|
1522
|
-
retryDelayMs: ProviderCliAdapter.FINISH_RETRY_DELAY_MS,
|
|
1523
|
-
assistantContent: summarizeCliTraceText(commitResult.assistantContent, 220),
|
|
1524
|
-
...buildCliTraceParseSnapshot({
|
|
1525
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1526
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1527
|
-
responseBuffer: this.responseBuffer,
|
|
1528
|
-
partialResponse: this.responseBuffer,
|
|
1529
|
-
scope: this.currentTurnScope,
|
|
1530
|
-
}),
|
|
1531
|
-
});
|
|
1532
|
-
if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
|
|
1533
|
-
this.finishRetryTimer = setTimeout(() => {
|
|
1534
|
-
this.finishRetryTimer = null;
|
|
1535
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1536
|
-
this.finishResponse();
|
|
1537
|
-
}
|
|
1538
|
-
}, ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
this.clearAllTimers();
|
|
1542
|
-
this.responseBuffer = '';
|
|
1543
|
-
this.isWaitingForResponse = false;
|
|
1544
|
-
this.responseSettleIgnoreUntil = 0;
|
|
1545
|
-
this.submitRetryUsed = false;
|
|
1546
|
-
this.submitRetryPromptSnippet = '';
|
|
1547
|
-
this.finishRetryCount = 0;
|
|
1548
|
-
this.currentTurnScope = null;
|
|
1549
|
-
this.activeModal = null;
|
|
1550
|
-
this.setStatus('idle', 'response_finished');
|
|
1551
|
-
this.onStatusChange?.();
|
|
1552
|
-
this.schedulePendingOutboundFlush();
|
|
769
|
+
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
770
|
+
return session;
|
|
1553
771
|
}
|
|
1554
772
|
|
|
1555
|
-
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
) {
|
|
1566
|
-
return false;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
const visibleAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant' && m.content.trim());
|
|
1570
|
-
if (!visibleAssistant) return false;
|
|
1571
|
-
|
|
1572
|
-
this.clearAllTimers();
|
|
1573
|
-
this.responseBuffer = '';
|
|
1574
|
-
this.isWaitingForResponse = false;
|
|
1575
|
-
this.responseSettleIgnoreUntil = 0;
|
|
1576
|
-
this.submitRetryUsed = false;
|
|
1577
|
-
this.submitRetryPromptSnippet = '';
|
|
1578
|
-
this.finishRetryCount = 0;
|
|
1579
|
-
this.currentTurnScope = null;
|
|
1580
|
-
this.activeModal = null;
|
|
1581
|
-
this.setStatus('idle', 'script_idle_commit');
|
|
1582
|
-
this.onStatusChange?.();
|
|
1583
|
-
this.schedulePendingOutboundFlush();
|
|
1584
|
-
this.recordTrace('script_idle_commit', {
|
|
1585
|
-
messageCount: parsedMessages.length,
|
|
1586
|
-
lastAssistant: summarizeCliTraceText(visibleAssistant.content, 320),
|
|
773
|
+
runDetectStatus(text: string): string | null {
|
|
774
|
+
const screenText = this.terminalScreen.getText();
|
|
775
|
+
const tail = text.slice(-500);
|
|
776
|
+
return this.runner.detectStatus({
|
|
777
|
+
tail,
|
|
778
|
+
screenText,
|
|
779
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
780
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
781
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
782
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1587
783
|
});
|
|
1588
|
-
return true;
|
|
1589
784
|
}
|
|
1590
785
|
|
|
1591
|
-
|
|
1592
|
-
const
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
const lastAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
|
|
1603
|
-
if (this.currentTurnScope) {
|
|
1604
|
-
LOG.info(
|
|
1605
|
-
'CLI',
|
|
1606
|
-
`[${this.cliType}] commitCurrentTranscript parserMessages=${parsedMessages.length} finalLastAssistant=${JSON.stringify(summarizeCliTraceText(lastAssistant?.content || '', 220)).slice(0, 260)}`
|
|
1607
|
-
);
|
|
1608
|
-
}
|
|
1609
|
-
this.recordTrace('commit_transcript', {
|
|
1610
|
-
parsedStatus: parsed.status || null,
|
|
1611
|
-
messageCount: parsedMessages.length,
|
|
1612
|
-
lastAssistant: lastAssistant ? summarizeCliTraceText(lastAssistant.content, 320) : '',
|
|
1613
|
-
messages: summarizeCliTraceMessages(parsedMessages),
|
|
1614
|
-
...buildCliTraceParseSnapshot({
|
|
1615
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1616
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1617
|
-
responseBuffer: this.responseBuffer,
|
|
1618
|
-
partialResponse: this.responseBuffer,
|
|
1619
|
-
scope: this.currentTurnScope,
|
|
1620
|
-
}),
|
|
1621
|
-
});
|
|
1622
|
-
if (!lastAssistant && this.currentTurnScope) {
|
|
1623
|
-
LOG.warn(
|
|
1624
|
-
'CLI',
|
|
1625
|
-
`[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
|
|
1626
|
-
);
|
|
1627
|
-
}
|
|
1628
|
-
const hasAssistant = !!lastAssistant;
|
|
1629
|
-
return {
|
|
1630
|
-
hasAssistant,
|
|
1631
|
-
assistantContent: lastAssistant?.content || '',
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
if (this.currentTurnScope) {
|
|
1635
|
-
LOG.info(
|
|
1636
|
-
'CLI',
|
|
1637
|
-
`[${this.cliType}] commitCurrentTranscript parsed.messages=none responseBufferLen=${this.responseBuffer.length} accumulatedBufferLen=${this.accumulatedBuffer.length} parsedStatus=${parsed?.status || '-'} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
|
|
1638
|
-
);
|
|
1639
|
-
}
|
|
1640
|
-
return {
|
|
1641
|
-
hasAssistant: false,
|
|
1642
|
-
assistantContent: '',
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
// ─── Script Execution ──────────────────────────
|
|
1648
|
-
|
|
1649
|
-
private invokeCliScript<T>(script: Function, input: any): T {
|
|
1650
|
-
const hasStateFactory = typeof this.cliScripts?.createState === 'function';
|
|
1651
|
-
const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
|
|
1652
|
-
return expectsStateArgument
|
|
1653
|
-
? script(this.scriptState, input)
|
|
1654
|
-
: script(input);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
private runParseSession(): ParsedSession | null {
|
|
1658
|
-
if (typeof this.cliScripts?.parseSession !== 'function') {
|
|
1659
|
-
this.parseErrorMessage = `${this.cliType} parseSession unavailable`;
|
|
1660
|
-
return null;
|
|
1661
|
-
}
|
|
1662
|
-
try {
|
|
1663
|
-
const screenText = this.terminalScreen.getText();
|
|
1664
|
-
const parseScreenText = this.getParseScreenText(screenText);
|
|
1665
|
-
const tail = this.recentOutputBuffer.slice(-500);
|
|
1666
|
-
const input = buildCliParseInput({
|
|
1667
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1668
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1669
|
-
recentOutputBuffer: this.recentOutputBuffer,
|
|
1670
|
-
terminalScreenText: parseScreenText,
|
|
1671
|
-
workingDir: this.workingDir,
|
|
1672
|
-
providerSessionId: this.providerSessionId || undefined,
|
|
1673
|
-
historySessionId: this.providerSessionId || undefined,
|
|
1674
|
-
baseMessages: [],
|
|
1675
|
-
partialResponse: this.responseBuffer,
|
|
1676
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1677
|
-
scope: this.currentTurnScope,
|
|
1678
|
-
runtimeSettings: this.runtimeSettings,
|
|
1679
|
-
});
|
|
1680
|
-
const session = this.invokeCliScript<ParsedSession | null>(
|
|
1681
|
-
this.cliScripts.parseSession,
|
|
1682
|
-
{ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
|
|
1683
|
-
);
|
|
1684
|
-
this.parseErrorMessage = null;
|
|
1685
|
-
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
1686
|
-
return session && typeof session === 'object' ? session : null;
|
|
1687
|
-
} catch (e: any) {
|
|
1688
|
-
const message = e?.message || String(e);
|
|
1689
|
-
this.parseErrorMessage = message;
|
|
1690
|
-
LOG.warn('CLI', `[${this.cliType}] parseSession error: ${message}`);
|
|
1691
|
-
return null;
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
private runDetectStatus(text: string): string | null {
|
|
1696
|
-
if (!this.cliScripts?.detectStatus) return null;
|
|
1697
|
-
try {
|
|
1698
|
-
const screenText = this.terminalScreen.getText();
|
|
1699
|
-
const status = this.invokeCliScript<string | null>(this.cliScripts.detectStatus, {
|
|
1700
|
-
tail: text.slice(-500),
|
|
1701
|
-
screenText,
|
|
1702
|
-
rawBuffer: this.accumulatedRawBuffer,
|
|
1703
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1704
|
-
screen: buildCliScreenSnapshot(screenText),
|
|
1705
|
-
tailScreen: buildCliScreenSnapshot(text.slice(-500)),
|
|
1706
|
-
});
|
|
1707
|
-
return status;
|
|
1708
|
-
} catch (e: any) {
|
|
1709
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
|
|
1710
|
-
return null;
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
1715
|
-
if (!this.cliScripts?.parseApproval) return null;
|
|
1716
|
-
try {
|
|
1717
|
-
const screenText = this.terminalScreen.getText();
|
|
1718
|
-
const buffer = screenText || this.accumulatedBuffer;
|
|
1719
|
-
return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
|
|
1720
|
-
buffer,
|
|
1721
|
-
screenText,
|
|
1722
|
-
rawBuffer: this.accumulatedRawBuffer,
|
|
1723
|
-
tail,
|
|
1724
|
-
screen: buildCliScreenSnapshot(screenText),
|
|
1725
|
-
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
1726
|
-
tailScreen: buildCliScreenSnapshot(tail),
|
|
1727
|
-
});
|
|
1728
|
-
} catch (e: any) {
|
|
1729
|
-
LOG.warn('CLI', `[${this.cliType}] parseApproval error: ${e.message}`);
|
|
1730
|
-
return null;
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
|
|
1735
|
-
return !!(startupModal || this.activeModal);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
|
|
1739
|
-
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1740
|
-
const lastAssistant = [...messages].reverse().find((message: any) => {
|
|
1741
|
-
if (!message || message.role !== 'assistant') return false;
|
|
1742
|
-
return typeof message.content === 'string' && message.content.trim().length > 0;
|
|
786
|
+
runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
787
|
+
const screenText = this.terminalScreen.getText();
|
|
788
|
+
const buffer = screenText || this.accumulatedBuffer;
|
|
789
|
+
return this.runner.parseApproval({
|
|
790
|
+
buffer,
|
|
791
|
+
screenText,
|
|
792
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
793
|
+
tail,
|
|
794
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
795
|
+
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
796
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1743
797
|
});
|
|
1744
|
-
return !!lastAssistant;
|
|
1745
798
|
}
|
|
1746
799
|
|
|
1747
800
|
private applyParsedSessionMetadata(parsed: any): void {
|
|
@@ -1752,62 +805,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1752
805
|
this.providerSessionId = providerSessionId;
|
|
1753
806
|
this.updateRuntimeMeta({ providerSessionId });
|
|
1754
807
|
}
|
|
1755
|
-
this.providerErrorMessage = typeof parsed?.errorMessage === 'string' && parsed.errorMessage.trim()
|
|
1756
|
-
? parsed.errorMessage.trim()
|
|
1757
|
-
: null;
|
|
1758
|
-
this.providerErrorReason = typeof parsed?.errorReason === 'string' && parsed.errorReason.trim()
|
|
1759
|
-
? parsed.errorReason.trim()
|
|
1760
|
-
: null;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
private parsedStatusHasFinalStandardAssistantMessage(parsed: any): boolean {
|
|
1764
|
-
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1765
|
-
const lastAssistant = [...messages].reverse().find((message: any) => {
|
|
1766
|
-
if (!message || message.role !== 'assistant') return false;
|
|
1767
|
-
return typeof message.content === 'string' && message.content.trim().length > 0;
|
|
1768
|
-
});
|
|
1769
|
-
if (!lastAssistant) return false;
|
|
1770
|
-
const kind = typeof lastAssistant.kind === 'string' && lastAssistant.kind.trim()
|
|
1771
|
-
? lastAssistant.kind.trim()
|
|
1772
|
-
: 'standard';
|
|
1773
|
-
return kind === 'standard' && lastAssistant.meta?.streaming !== true;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
private shouldKeepCodexTurnOpenForFinish(parsed: any): boolean {
|
|
1777
|
-
if (this.cliType !== 'codex-cli') return false;
|
|
1778
|
-
if (!this.isWaitingForResponse || !this.currentTurnScope || this.hasActionableApproval()) return false;
|
|
1779
|
-
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status.trim() : '';
|
|
1780
|
-
if (parsedStatus !== 'idle') return true;
|
|
1781
|
-
if (parsed?.activeModal || parsed?.modal) return true;
|
|
1782
|
-
return !this.parsedStatusHasFinalStandardAssistantMessage(parsed);
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
private rescheduleCodexFinishCheck(reason: string): void {
|
|
1786
|
-
this.clearIdleFinishCandidate(reason);
|
|
1787
|
-
this.setStatus('generating', reason);
|
|
1788
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1789
|
-
this.idleTimeout = setTimeout(() => {
|
|
1790
|
-
if (!this.isWaitingForResponse || this.hasActionableApproval()) return;
|
|
1791
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1792
|
-
this.evaluateSettled();
|
|
1793
|
-
}, this.getIdleFinishConfirmMs());
|
|
1794
|
-
this.recordTrace('codex_finish_deferred', {
|
|
1795
|
-
reason,
|
|
1796
|
-
...buildCliTraceParseSnapshot({
|
|
1797
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1798
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1799
|
-
responseBuffer: this.responseBuffer,
|
|
1800
|
-
partialResponse: this.responseBuffer,
|
|
1801
|
-
scope: this.currentTurnScope,
|
|
1802
|
-
}),
|
|
1803
|
-
});
|
|
1804
808
|
}
|
|
1805
809
|
|
|
1806
810
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1807
811
|
if (this.parseErrorMessage) return 'error';
|
|
1808
|
-
if (this.
|
|
1809
|
-
if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
|
|
1810
|
-
return this.currentStatus;
|
|
812
|
+
if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
|
|
813
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
|
|
814
|
+
return this.engine.currentStatus;
|
|
1811
815
|
}
|
|
1812
816
|
|
|
1813
817
|
// ─── Public API (CliAdapter) ───────────────────
|
|
@@ -1819,7 +823,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1819
823
|
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
1820
824
|
: null;
|
|
1821
825
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1822
|
-
let effectiveModal = startupModal || this.activeModal;
|
|
826
|
+
let effectiveModal = startupModal || this.engine.activeModal;
|
|
1823
827
|
if (startupDetectedStatus === 'waiting_approval') {
|
|
1824
828
|
effectiveStatus = 'waiting_approval';
|
|
1825
829
|
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
@@ -1831,19 +835,23 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1831
835
|
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
1832
836
|
? parsed.activeModal
|
|
1833
837
|
: null;
|
|
838
|
+
const hasFinalAssistant = (p: any) => {
|
|
839
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
840
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
841
|
+
};
|
|
1834
842
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1835
843
|
effectiveStatus = 'waiting_approval';
|
|
1836
844
|
effectiveModal = parsedModal;
|
|
1837
845
|
} else if (
|
|
1838
846
|
effectiveStatus === 'idle'
|
|
1839
847
|
&& parsed?.status === 'generating'
|
|
1840
|
-
&& !
|
|
848
|
+
&& !hasFinalAssistant(parsed)
|
|
1841
849
|
) {
|
|
1842
850
|
effectiveStatus = 'generating';
|
|
1843
851
|
} else if (
|
|
1844
852
|
effectiveStatus === 'generating'
|
|
1845
853
|
&& parsed?.status === 'idle'
|
|
1846
|
-
&&
|
|
854
|
+
&& hasFinalAssistant(parsed)
|
|
1847
855
|
) {
|
|
1848
856
|
effectiveStatus = 'idle';
|
|
1849
857
|
}
|
|
@@ -1862,8 +870,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1862
870
|
queuedAt: message.queuedAt,
|
|
1863
871
|
source: message.source,
|
|
1864
872
|
})),
|
|
1865
|
-
errorMessage: this.parseErrorMessage || this.providerErrorMessage || undefined,
|
|
1866
|
-
errorReason: this.parseErrorMessage ? 'parse_error' : (this.providerErrorReason || undefined),
|
|
873
|
+
errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
|
|
874
|
+
errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
|
|
1867
875
|
providerSessionId: this.providerSessionId || undefined,
|
|
1868
876
|
...(bufferState ? { bufferState } : {}),
|
|
1869
877
|
};
|
|
@@ -1883,13 +891,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1883
891
|
!this.providerOwnsTranscript()
|
|
1884
892
|
&& cached
|
|
1885
893
|
&& cached.responseBuffer === this.responseBuffer
|
|
1886
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
894
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1887
895
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1888
896
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1889
897
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1890
898
|
&& cached.screenText === parseScreenText
|
|
1891
|
-
&& cached.currentStatus === this.currentStatus
|
|
1892
|
-
&& cached.activeModal === this.activeModal
|
|
899
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
900
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1893
901
|
&& cached.cliName === this.cliName
|
|
1894
902
|
) {
|
|
1895
903
|
return cached.result;
|
|
@@ -1904,7 +912,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1904
912
|
const bufferState = this.getBufferState();
|
|
1905
913
|
const result = {
|
|
1906
914
|
id: (parsed as any).id || 'cli_session',
|
|
1907
|
-
status: parsed.status || this.currentStatus,
|
|
915
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1908
916
|
title: (parsed as any).title || this.cliName,
|
|
1909
917
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1910
918
|
scope: null,
|
|
@@ -1929,13 +937,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1929
937
|
|
|
1930
938
|
this.parsedStatusCache = {
|
|
1931
939
|
responseBuffer: this.responseBuffer,
|
|
1932
|
-
currentTurnScope: this.currentTurnScope,
|
|
940
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1933
941
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1934
942
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1935
943
|
accumulatedRawBufferKey,
|
|
1936
944
|
screenText: parseScreenText,
|
|
1937
|
-
currentStatus: this.currentStatus,
|
|
1938
|
-
activeModal: this.activeModal,
|
|
945
|
+
currentStatus: this.engine.currentStatus,
|
|
946
|
+
activeModal: this.engine.activeModal,
|
|
1939
947
|
cliName: this.cliName,
|
|
1940
948
|
result,
|
|
1941
949
|
};
|
|
@@ -1943,10 +951,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1943
951
|
}
|
|
1944
952
|
|
|
1945
953
|
async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
|
|
1946
|
-
const fn = this.cliScripts?.[scriptName];
|
|
1947
|
-
if (typeof fn !== 'function') {
|
|
1948
|
-
throw new Error(`CLI script '${scriptName}' not available`);
|
|
1949
|
-
}
|
|
1950
954
|
const input = buildCliParseInput({
|
|
1951
955
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1952
956
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
@@ -1957,11 +961,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1957
961
|
historySessionId: this.providerSessionId || undefined,
|
|
1958
962
|
baseMessages: [],
|
|
1959
963
|
partialResponse: this.responseBuffer,
|
|
1960
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1961
|
-
scope: this.currentTurnScope,
|
|
964
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
965
|
+
scope: this.engine.currentTurnScope,
|
|
1962
966
|
runtimeSettings: this.runtimeSettings,
|
|
1963
967
|
});
|
|
1964
|
-
return await Promise.resolve(this.
|
|
968
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1965
969
|
...input,
|
|
1966
970
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1967
971
|
}));
|
|
@@ -1973,7 +977,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1973
977
|
|
|
1974
978
|
/** Whether this adapter has CLI scripts loaded */
|
|
1975
979
|
hasCliScripts(): boolean {
|
|
1976
|
-
return
|
|
980
|
+
return this.runner.hasDetectStatus();
|
|
1977
981
|
}
|
|
1978
982
|
|
|
1979
983
|
/**
|
|
@@ -1982,24 +986,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1982
986
|
*/
|
|
1983
987
|
async resolveAction(data: any): Promise<void> {
|
|
1984
988
|
let promptText = '';
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
}
|
|
989
|
+
try {
|
|
990
|
+
promptText = this.runner.invokeByName('resolveAction', data);
|
|
991
|
+
} catch {
|
|
992
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
|
|
993
|
+
return;
|
|
1991
994
|
}
|
|
1992
995
|
if (!promptText) {
|
|
1993
|
-
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not
|
|
996
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
|
|
1994
997
|
return;
|
|
1995
998
|
}
|
|
1996
999
|
await this.sendMessage(promptText);
|
|
1997
1000
|
}
|
|
1998
1001
|
|
|
1999
1002
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
2000
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
2001
|
-
if (this.hasActionableApproval()) return false;
|
|
2002
|
-
|
|
1003
|
+
if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
|
|
1004
|
+
if (this.engine.hasActionableApproval()) return false;
|
|
1005
|
+
// If there's already meaningful response content beyond the echoed prompt, not stuck
|
|
1006
|
+
if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
|
|
2003
1007
|
const screenText = this.terminalScreen.getText();
|
|
2004
1008
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
2005
1009
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -2008,20 +1012,41 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2008
1012
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
2009
1013
|
}
|
|
2010
1014
|
|
|
1015
|
+
private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
|
|
1016
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
1017
|
+
if (!raw) return false;
|
|
1018
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1019
|
+
if (!normalizedPrompt) return true;
|
|
1020
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
1021
|
+
if (!normalizedBuffer) return false;
|
|
1022
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1023
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1024
|
+
const remainder = normalizedBuffer
|
|
1025
|
+
.slice(normalizedPrompt.length)
|
|
1026
|
+
.replace(/[─═\-]+/g, '')
|
|
1027
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1028
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1029
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1030
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1031
|
+
.replace(/esctointerrupt/gi, '')
|
|
1032
|
+
.replace(/❯/g, '')
|
|
1033
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1034
|
+
.trim();
|
|
1035
|
+
return remainder.length > 0;
|
|
1036
|
+
}
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
2011
1040
|
private async writeToPty(data: string): Promise<void> {
|
|
2012
1041
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2013
1042
|
await this.ptyProcess.write(data);
|
|
2014
1043
|
}
|
|
2015
1044
|
|
|
2016
1045
|
private resetPendingSendState(reason: string): void {
|
|
2017
|
-
this.isWaitingForResponse = false;
|
|
2018
1046
|
this.responseBuffer = '';
|
|
2019
|
-
this.currentTurnScope = null;
|
|
2020
|
-
this.submitPendingUntil = 0;
|
|
2021
|
-
this.clearIdleFinishCandidate(reason);
|
|
2022
1047
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
2023
|
-
|
|
2024
|
-
|
|
1048
|
+
this.engine.resetActiveTurnState();
|
|
1049
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
2025
1050
|
}
|
|
2026
1051
|
|
|
2027
1052
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
@@ -2038,42 +1063,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2038
1063
|
}
|
|
2039
1064
|
this.responseTimeout = setTimeout(() => {
|
|
2040
1065
|
this.responseTimeout = null;
|
|
2041
|
-
if (!this.isWaitingForResponse) return;
|
|
2042
|
-
|
|
2043
|
-
const detectedStatusBeforeEval = this.runDetectStatus(this.recentOutputBuffer);
|
|
2044
|
-
this.recordTrace('response_timeout_check', {
|
|
2045
|
-
timeoutMs,
|
|
2046
|
-
detectedStatus: detectedStatusBeforeEval,
|
|
2047
|
-
currentStatus: this.currentStatus,
|
|
2048
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2049
|
-
hasActionableApproval: this.hasActionableApproval(),
|
|
2050
|
-
...buildCliTraceParseSnapshot({
|
|
2051
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
2052
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
2053
|
-
responseBuffer: this.responseBuffer,
|
|
2054
|
-
partialResponse: this.responseBuffer,
|
|
2055
|
-
scope: this.currentTurnScope,
|
|
2056
|
-
}),
|
|
2057
|
-
});
|
|
1066
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
2058
1067
|
|
|
2059
|
-
// maxResponse is a watchdog/checkpoint, not a completion signal.
|
|
2060
|
-
// behavior called finishResponse() unconditionally at the default 300s,
|
|
2061
|
-
// which fabricated idle transitions and downstream generating_completed
|
|
2062
|
-
// notifications while long-running CLIs were still generating. Re-run the
|
|
1068
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
2063
1069
|
// normal settled parser instead and keep the turn open unless the provider
|
|
2064
1070
|
// actually reports an idle, commit-ready state.
|
|
2065
|
-
this.
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
2069
|
-
const detectedStatusAfterEval = this.runDetectStatus(this.recentOutputBuffer);
|
|
2070
|
-
this.recordTrace('response_timeout_kept_open', {
|
|
2071
|
-
timeoutMs,
|
|
2072
|
-
detectedStatusBeforeEval,
|
|
2073
|
-
detectedStatusAfterEval,
|
|
2074
|
-
currentStatus: this.currentStatus,
|
|
2075
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2076
|
-
});
|
|
1071
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1072
|
+
|
|
1073
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
2077
1074
|
this.armResponseTimeout();
|
|
2078
1075
|
}
|
|
2079
1076
|
}, timeoutMs);
|
|
@@ -2088,34 +1085,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2088
1085
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
2089
1086
|
this.submitRetryTimer = null;
|
|
2090
1087
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
2091
|
-
|
|
2092
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1088
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2093
1089
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
|
|
2094
|
-
this.recordTrace('submit_write', {
|
|
2095
|
-
mode: 'submit_retry',
|
|
2096
|
-
attempt,
|
|
2097
|
-
sendKey: this.sendKey,
|
|
2098
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2099
|
-
});
|
|
2100
1090
|
this.writeSubmitKeyForRetry('submit_retry');
|
|
2101
|
-
if (attempt >= 3) { this.submitRetryUsed = true; return; }
|
|
1091
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
2102
1092
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
2103
1093
|
}
|
|
2104
1094
|
|
|
2105
1095
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
2106
1096
|
this.submitRetryTimer = null;
|
|
2107
1097
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
2108
|
-
|
|
2109
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1098
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2110
1099
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
|
|
2111
|
-
this.recordTrace('submit_write', {
|
|
2112
|
-
mode: 'immediate_retry',
|
|
2113
|
-
attempt: 1,
|
|
2114
|
-
sendKey: this.sendKey,
|
|
2115
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2116
|
-
});
|
|
2117
1100
|
this.writeSubmitKeyForRetry('immediate_retry');
|
|
2118
|
-
this.submitRetryUsed = true;
|
|
1101
|
+
this.engine.submitRetryUsed = true;
|
|
2119
1102
|
}
|
|
2120
1103
|
|
|
2121
1104
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -2123,13 +1106,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2123
1106
|
completion.resolveOnce();
|
|
2124
1107
|
return;
|
|
2125
1108
|
}
|
|
2126
|
-
this.submitPendingUntil = 0;
|
|
2127
|
-
const screenText = this.terminalScreen.getText();
|
|
2128
|
-
this.recordTrace('submit_write', {
|
|
2129
|
-
mode: 'submit_key',
|
|
2130
|
-
sendKey: this.sendKey,
|
|
2131
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2132
|
-
});
|
|
1109
|
+
this.engine.submitPendingUntil = 0;
|
|
2133
1110
|
void this.writeToPty(this.sendKey).then(() => {
|
|
2134
1111
|
this.commitSendUserTurn(state);
|
|
2135
1112
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -2139,13 +1116,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2139
1116
|
}
|
|
2140
1117
|
|
|
2141
1118
|
private submitImmediatePrompt(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
2142
|
-
this.submitPendingUntil = 0;
|
|
2143
|
-
this.recordTrace('submit_write', {
|
|
2144
|
-
mode: 'immediate',
|
|
2145
|
-
text: summarizeCliTraceText(state.text, 500),
|
|
2146
|
-
sendKey: this.sendKey,
|
|
2147
|
-
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
|
|
2148
|
-
});
|
|
1119
|
+
this.engine.submitPendingUntil = 0;
|
|
2149
1120
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
2150
1121
|
this.commitSendUserTurn(state);
|
|
2151
1122
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -2189,7 +1160,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2189
1160
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2190
1161
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
2191
1162
|
};
|
|
2192
|
-
|
|
1163
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1164
|
+
|
|
2193
1165
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
2194
1166
|
// At this point the prompt text write already completed. Rejecting without
|
|
2195
1167
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -2226,13 +1198,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2226
1198
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2227
1199
|
const content = String(text || '');
|
|
2228
1200
|
if (!content.trim()) return;
|
|
2229
|
-
|
|
2230
|
-
text: summarizeCliTraceText(content, 500),
|
|
2231
|
-
status: this.currentStatus,
|
|
2232
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2233
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2234
|
-
});
|
|
2235
|
-
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.currentStatus}`);
|
|
1201
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
|
|
2236
1202
|
await this.writeToPty(content + this.sendKey);
|
|
2237
1203
|
this.onStatusChange?.();
|
|
2238
1204
|
}
|
|
@@ -2241,11 +1207,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2241
1207
|
const content = String(text || '');
|
|
2242
1208
|
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
2243
1209
|
if (duplicate) {
|
|
2244
|
-
this.recordTrace('send_message_queued_duplicate_suppressed', {
|
|
2245
|
-
reason,
|
|
2246
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2247
|
-
text: summarizeCliTraceText(content, 500),
|
|
2248
|
-
});
|
|
2249
1210
|
return;
|
|
2250
1211
|
}
|
|
2251
1212
|
const queuedAt = Date.now();
|
|
@@ -2257,24 +1218,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2257
1218
|
source: 'sendMessage',
|
|
2258
1219
|
};
|
|
2259
1220
|
this.pendingOutboundQueue.push(message);
|
|
2260
|
-
this.recordTrace('send_message_queued', {
|
|
2261
|
-
reason,
|
|
2262
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2263
|
-
queuedAt,
|
|
2264
|
-
text: summarizeCliTraceText(content, 500),
|
|
2265
|
-
});
|
|
2266
1221
|
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
2267
1222
|
this.onStatusChange?.();
|
|
2268
1223
|
}
|
|
2269
1224
|
|
|
2270
1225
|
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
2271
1226
|
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
2272
|
-
if (this.hasActionableApproval()) return null;
|
|
1227
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
2273
1228
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
2274
1229
|
? String(parsedStatusBeforeSend.status)
|
|
2275
1230
|
: '';
|
|
2276
|
-
|
|
2277
|
-
|
|
1231
|
+
const hasFinalAssistant = (p: any) => {
|
|
1232
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1233
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1234
|
+
};
|
|
1235
|
+
if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
|
|
1236
|
+
if (this.engine.currentStatus === 'generating') return 'current_status_generating';
|
|
2278
1237
|
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
|
|
2279
1238
|
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
2280
1239
|
const parsedHasActionableModal = Boolean(
|
|
@@ -2282,15 +1241,15 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2282
1241
|
&& Array.isArray(parsedModal.buttons)
|
|
2283
1242
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
2284
1243
|
);
|
|
2285
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1244
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
2286
1245
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
2287
|
-
&& !this.isWaitingForResponse
|
|
2288
|
-
&& !this.currentTurnScope
|
|
2289
|
-
&& !this.hasActionableApproval()
|
|
1246
|
+
&& !this.engine.isWaitingForResponse
|
|
1247
|
+
&& !this.engine.currentTurnScope
|
|
1248
|
+
&& !this.engine.hasActionableApproval()
|
|
2290
1249
|
&& !parsedHasActionableModal;
|
|
2291
1250
|
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
2292
1251
|
}
|
|
2293
|
-
if (this.isWaitingForResponse && this.currentTurnScope) return 'active_turn_in_progress';
|
|
1252
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
2294
1253
|
return null;
|
|
2295
1254
|
}
|
|
2296
1255
|
|
|
@@ -2304,18 +1263,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2304
1263
|
|
|
2305
1264
|
private async flushPendingOutboundQueue(): Promise<void> {
|
|
2306
1265
|
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
2307
|
-
if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) return;
|
|
1266
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
2308
1267
|
this.pendingOutboundFlushInFlight = true;
|
|
2309
1268
|
try {
|
|
2310
1269
|
while (this.pendingOutboundQueue.length > 0) {
|
|
2311
|
-
if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) break;
|
|
1270
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
2312
1271
|
const next = this.pendingOutboundQueue[0];
|
|
2313
|
-
this.recordTrace('send_message_queue_flush', {
|
|
2314
|
-
id: next.id,
|
|
2315
|
-
queuedAt: next.queuedAt,
|
|
2316
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2317
|
-
text: summarizeCliTraceText(next.content, 500),
|
|
2318
|
-
});
|
|
2319
1272
|
try {
|
|
2320
1273
|
await this.sendMessageNow(next.content, false);
|
|
2321
1274
|
this.pendingOutboundQueue.shift();
|
|
@@ -2335,8 +1288,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2335
1288
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2336
1289
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
2337
1290
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
2338
|
-
&& this.isWaitingForResponse
|
|
2339
|
-
&& !this.hasActionableApproval();
|
|
1291
|
+
&& this.engine.isWaitingForResponse
|
|
1292
|
+
&& !this.engine.hasActionableApproval();
|
|
2340
1293
|
if (this.startupParseGate) {
|
|
2341
1294
|
const deadline = Date.now() + 10000;
|
|
2342
1295
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -2366,11 +1319,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2366
1319
|
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
2367
1320
|
this.ready = true;
|
|
2368
1321
|
this.startupParseGate = false;
|
|
2369
|
-
this.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1322
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
2370
1323
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
2371
1324
|
}
|
|
2372
1325
|
}
|
|
2373
|
-
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
|
|
1326
|
+
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
2374
1327
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
2375
1328
|
? String(parsedStatusBeforeSend.status)
|
|
2376
1329
|
: '';
|
|
@@ -2381,11 +1334,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2381
1334
|
&& Array.isArray(parsedModal.buttons)
|
|
2382
1335
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
2383
1336
|
);
|
|
2384
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1337
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
2385
1338
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
2386
|
-
&& !this.isWaitingForResponse
|
|
2387
|
-
&& !this.currentTurnScope
|
|
2388
|
-
&& !this.hasActionableApproval()
|
|
1339
|
+
&& !this.engine.isWaitingForResponse
|
|
1340
|
+
&& !this.engine.currentTurnScope
|
|
1341
|
+
&& !this.engine.hasActionableApproval()
|
|
2389
1342
|
&& !parsedHasActionableModal;
|
|
2390
1343
|
if (!terminalLooksIdle) {
|
|
2391
1344
|
if (allowQueue) {
|
|
@@ -2395,10 +1348,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2395
1348
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
2396
1349
|
}
|
|
2397
1350
|
}
|
|
2398
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1351
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1352
|
+
const snap = this.getSnapshot();
|
|
2399
1353
|
if (
|
|
2400
|
-
!this.clearStaleIdleResponseGuard('send_message_guard')
|
|
2401
|
-
&& !this.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend)
|
|
1354
|
+
!this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
|
|
1355
|
+
&& !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
|
|
2402
1356
|
) {
|
|
2403
1357
|
if (allowQueue) {
|
|
2404
1358
|
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
@@ -2407,30 +1361,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2407
1361
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
2408
1362
|
}
|
|
2409
1363
|
}
|
|
2410
|
-
this.isWaitingForResponse = true;
|
|
2411
1364
|
this.responseBuffer = '';
|
|
2412
|
-
|
|
2413
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
2414
|
-
this.clearIdleFinishCandidate('send_message');
|
|
2415
|
-
this.currentTurnScope = {
|
|
1365
|
+
const turnScope: TurnParseScope = {
|
|
2416
1366
|
prompt: text,
|
|
2417
1367
|
startedAt: Date.now(),
|
|
2418
1368
|
bufferStart: this.accumulatedBuffer.length,
|
|
2419
1369
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
2420
1370
|
};
|
|
2421
|
-
|
|
2422
|
-
text: summarizeCliTraceText(text, 500),
|
|
2423
|
-
estimatedLines: estimatePromptDisplayLines(text),
|
|
2424
|
-
turnScope: this.currentTurnScope,
|
|
2425
|
-
});
|
|
2426
|
-
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2427
|
-
this.submitRetryUsed = false;
|
|
2428
|
-
this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
2429
|
-
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
|
|
1371
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2430
1372
|
if (this.submitRetryTimer) {
|
|
2431
1373
|
clearTimeout(this.submitRetryTimer);
|
|
2432
1374
|
this.submitRetryTimer = null;
|
|
2433
1375
|
}
|
|
1376
|
+
this.engine.onTurnStarted(turnScope);
|
|
1377
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1378
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2434
1379
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2435
1380
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2436
1381
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2443,12 +1388,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2443
1388
|
retryDelayMs,
|
|
2444
1389
|
didCommitUserTurn: false,
|
|
2445
1390
|
};
|
|
2446
|
-
|
|
2447
|
-
clearTimeout(this.settleTimer);
|
|
2448
|
-
this.settleTimer = null;
|
|
2449
|
-
}
|
|
2450
|
-
this.responseEpoch += 1;
|
|
2451
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1391
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2452
1392
|
await new Promise<void>((resolve, reject) => {
|
|
2453
1393
|
let resolved = false;
|
|
2454
1394
|
const completion: SendMessageCompletion = {
|
|
@@ -2471,24 +1411,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2471
1411
|
}
|
|
2472
1412
|
|
|
2473
1413
|
if (submitDelayMs > 0) {
|
|
2474
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1414
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2475
1415
|
}
|
|
2476
|
-
this.recordTrace('submit_write', {
|
|
2477
|
-
mode: 'type_then_submit',
|
|
2478
|
-
text: summarizeCliTraceText(text, 500),
|
|
2479
|
-
sendKey: this.sendKey,
|
|
2480
|
-
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
|
|
2481
|
-
});
|
|
2482
1416
|
const submitStartedAt = Date.now();
|
|
2483
1417
|
void this.writeToPty(text).then(
|
|
2484
1418
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2485
1419
|
completion.rejectOnce,
|
|
2486
1420
|
);
|
|
2487
1421
|
});
|
|
1422
|
+
// Schedule settle after successful send
|
|
1423
|
+
this.engine.scheduleSettle();
|
|
2488
1424
|
}
|
|
2489
1425
|
|
|
2490
1426
|
getPartialResponse(): string {
|
|
2491
|
-
if (!this.isWaitingForResponse) return '';
|
|
1427
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2492
1428
|
return this.responseBuffer;
|
|
2493
1429
|
}
|
|
2494
1430
|
|
|
@@ -2501,10 +1437,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2501
1437
|
cliType: this.cliType,
|
|
2502
1438
|
cliName: this.cliName,
|
|
2503
1439
|
workingDir: this.workingDir,
|
|
2504
|
-
currentStatus: this.currentStatus,
|
|
1440
|
+
currentStatus: this.engine.currentStatus,
|
|
2505
1441
|
ready: this.ready,
|
|
2506
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2507
|
-
activeModal: this.activeModal,
|
|
1442
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1443
|
+
activeModal: this.engine.activeModal,
|
|
2508
1444
|
parseErrorMessage: this.parseErrorMessage,
|
|
2509
1445
|
messageCounts: {
|
|
2510
1446
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2530,10 +1466,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2530
1466
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2531
1467
|
},
|
|
2532
1468
|
parser: {
|
|
2533
|
-
scriptNames:
|
|
2534
|
-
traceSessionId: this.
|
|
2535
|
-
traceSeq: this.
|
|
2536
|
-
currentTurnScope: this.currentTurnScope,
|
|
1469
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1470
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1471
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1472
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2537
1473
|
parsedStatusCache: parsedResult
|
|
2538
1474
|
? {
|
|
2539
1475
|
id: parsedResult.id,
|
|
@@ -2546,26 +1482,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2546
1482
|
activeModal: parsedResult.activeModal,
|
|
2547
1483
|
}
|
|
2548
1484
|
: null,
|
|
2549
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2550
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1485
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1486
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2551
1487
|
},
|
|
2552
1488
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2553
|
-
statusHistory: this.
|
|
2554
|
-
traceEntries: this.
|
|
1489
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1490
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2555
1491
|
timing: {
|
|
2556
1492
|
spawnAt: this.spawnAt,
|
|
2557
1493
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2558
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2559
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2560
|
-
responseEpoch: this.responseEpoch,
|
|
1494
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1495
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1496
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2561
1497
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2562
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1498
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2563
1499
|
},
|
|
2564
1500
|
finish: {
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1501
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1502
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1503
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2569
1504
|
},
|
|
2570
1505
|
};
|
|
2571
1506
|
}
|
|
@@ -2602,7 +1537,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2602
1537
|
this.timeouts.shutdownGrace,
|
|
2603
1538
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2604
1539
|
);
|
|
2605
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1540
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2606
1541
|
|
|
2607
1542
|
try {
|
|
2608
1543
|
if (wasProcessing) {
|
|
@@ -2640,7 +1575,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2640
1575
|
return new Promise((resolve) => {
|
|
2641
1576
|
const startedAt = Date.now();
|
|
2642
1577
|
const timer = setInterval(() => {
|
|
2643
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
1578
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2644
1579
|
clearInterval(timer);
|
|
2645
1580
|
resolve(true);
|
|
2646
1581
|
return;
|
|
@@ -2654,12 +1589,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2654
1589
|
}
|
|
2655
1590
|
|
|
2656
1591
|
shutdown(): void {
|
|
2657
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
1592
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2658
1593
|
this.clearAllTimers();
|
|
2659
1594
|
this.pendingOutputParseChunks = [];
|
|
2660
1595
|
this.pendingTerminalQueryTail = '';
|
|
2661
1596
|
this.ptyOutputChunks = [];
|
|
2662
|
-
this.finishRetryCount = 0;
|
|
2663
1597
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2664
1598
|
this.pendingOutboundQueue = [];
|
|
2665
1599
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2668,7 +1602,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2668
1602
|
setTimeout(() => {
|
|
2669
1603
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2670
1604
|
this.ptyProcess = null;
|
|
2671
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
1605
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2672
1606
|
this.ready = false;
|
|
2673
1607
|
this.startupParseGate = false;
|
|
2674
1608
|
this.spawnAt = 0;
|
|
@@ -2678,12 +1612,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2678
1612
|
}
|
|
2679
1613
|
|
|
2680
1614
|
detach(): void {
|
|
2681
|
-
this.clearIdleFinishCandidate('detach');
|
|
1615
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2682
1616
|
this.clearAllTimers();
|
|
2683
1617
|
this.pendingOutputParseChunks = [];
|
|
2684
1618
|
this.pendingTerminalQueryTail = '';
|
|
2685
1619
|
this.ptyOutputChunks = [];
|
|
2686
|
-
this.finishRetryCount = 0;
|
|
2687
1620
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2688
1621
|
this.pendingOutboundQueue = [];
|
|
2689
1622
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2704,19 +1637,17 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2704
1637
|
}
|
|
2705
1638
|
|
|
2706
1639
|
clearHistory(): void {
|
|
2707
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
1640
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2708
1641
|
this.accumulatedBuffer = '';
|
|
2709
1642
|
this.accumulatedRawBuffer = '';
|
|
2710
|
-
this.currentTurnScope = null;
|
|
2711
|
-
this.submitRetryUsed = false;
|
|
2712
|
-
this.submitRetryPromptSnippet = '';
|
|
1643
|
+
this.engine.currentTurnScope = null;
|
|
1644
|
+
this.engine.submitRetryUsed = false;
|
|
1645
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2713
1646
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2714
1647
|
this.pendingOutputParseChunks = [];
|
|
2715
1648
|
this.pendingTerminalQueryTail = '';
|
|
2716
1649
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2717
1650
|
this.ptyOutputChunks = [];
|
|
2718
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
2719
|
-
this.finishRetryCount = 0;
|
|
2720
1651
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2721
1652
|
this.pendingOutboundQueue = [];
|
|
2722
1653
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2725,76 +1656,109 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2725
1656
|
this.onStatusChange?.();
|
|
2726
1657
|
}
|
|
2727
1658
|
|
|
2728
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
1659
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2729
1660
|
isReady(): boolean { return this.ready; }
|
|
2730
1661
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
1662
|
+
// ─── State machine property accessors (delegate to engine) ──────────────
|
|
1663
|
+
// These expose engine state for external callers (tests, debug tools, etc.)
|
|
1664
|
+
|
|
1665
|
+
get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
|
|
1666
|
+
set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
|
|
1667
|
+
|
|
1668
|
+
get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
|
|
1669
|
+
set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
|
|
1670
|
+
|
|
1671
|
+
get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
|
|
1672
|
+
set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
|
|
1673
|
+
|
|
1674
|
+
get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
|
|
1675
|
+
set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
|
|
1676
|
+
|
|
1677
|
+
get responseEpoch(): number { return this.engine.responseEpoch; }
|
|
1678
|
+
set responseEpoch(v: number) { this.engine.responseEpoch = v; }
|
|
1679
|
+
|
|
1680
|
+
get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
|
|
1681
|
+
set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
|
|
1682
|
+
|
|
1683
|
+
get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
|
|
1684
|
+
set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
|
|
1685
|
+
|
|
1686
|
+
get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
|
|
1687
|
+
set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
|
|
1688
|
+
|
|
1689
|
+
get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
|
|
1690
|
+
set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
|
|
1691
|
+
|
|
1692
|
+
get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
|
|
1693
|
+
set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
|
|
1694
|
+
|
|
1695
|
+
get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
|
|
1696
|
+
get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
|
|
1697
|
+
|
|
1698
|
+
get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
|
|
1699
|
+
get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
|
|
1700
|
+
|
|
1701
|
+
get finishRetryCount(): number { return this.engine.finishRetryCount; }
|
|
1702
|
+
set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
|
|
1703
|
+
|
|
1704
|
+
get traceSessionId(): string { return this.engine.getTraceSessionId(); }
|
|
1705
|
+
get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
|
|
1706
|
+
get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
|
|
1707
|
+
get traceSeq(): number { return this.engine.getTraceEntries().length; }
|
|
1708
|
+
|
|
1709
|
+
/** Expose engine's evaluateSettled for test access */
|
|
1710
|
+
evaluateSettled(): void {
|
|
1711
|
+
LOG.debug(
|
|
1712
|
+
'CLI',
|
|
1713
|
+
`[${this.cliType}] settled diagnostics delegated to state engine`);
|
|
1714
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1715
|
+
}
|
|
1716
|
+
/** Expose engine's scheduleSettle for test access */
|
|
1717
|
+
scheduleSettle(): void { this.engine.scheduleSettle(); }
|
|
1718
|
+
/** Expose engine's clearIdleFinishCandidate for test access */
|
|
1719
|
+
clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
|
|
1720
|
+
/** Expose engine's finishResponse for test access */
|
|
1721
|
+
finishResponse(): void { this.engine.finishResponse(); }
|
|
1722
|
+
/** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
|
|
1723
|
+
getSnapshot(): CliBufferSnapshot {
|
|
1724
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
1725
|
+
return {
|
|
1726
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
1727
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1728
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
1729
|
+
responseBuffer: this.responseBuffer,
|
|
1730
|
+
screenText,
|
|
1731
|
+
parseScreenText: this.getParseScreenText(screenText),
|
|
1732
|
+
workingDir: this.workingDir,
|
|
1733
|
+
providerSessionId: this.providerSessionId,
|
|
1734
|
+
runtimeSettings: this.runtimeSettings,
|
|
1735
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1736
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1737
|
+
lastOutputAt: this.lastOutputAt,
|
|
1738
|
+
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
1739
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1740
|
+
lastScreenSnapshot: this.lastScreenSnapshot,
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
isAlive(): boolean { return this.ptyProcess !== null; }
|
|
1744
|
+
flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
|
|
1745
|
+
|
|
1746
|
+
async writeRaw(data: string | Buffer): Promise<void> {
|
|
1747
|
+
const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
|
1748
|
+
await this.writeToPty(str);
|
|
2737
1749
|
}
|
|
2738
1750
|
|
|
2739
1751
|
resolveModal(buttonIndex: number): void {
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
return;
|
|
2746
|
-
}
|
|
2747
|
-
let modal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
|
|
2748
|
-
if (!modal && typeof this.cliScripts?.parseSession === 'function') {
|
|
2749
|
-
try {
|
|
2750
|
-
const parsed = this.getScriptParsedStatus();
|
|
2751
|
-
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
2752
|
-
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
2753
|
-
? parsed.activeModal
|
|
2754
|
-
: null;
|
|
2755
|
-
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
2756
|
-
modal = parsedModal;
|
|
2757
|
-
this.activeModal = parsedModal;
|
|
2758
|
-
if (this.currentStatus !== 'waiting_approval') {
|
|
2759
|
-
this.setStatus('waiting_approval', 'resolve_modal_parse');
|
|
2760
|
-
this.onStatusChange?.();
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
} catch {
|
|
2764
|
-
// Ignore parse failures here; resolveModal falls back to current state.
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
if (!this.ptyProcess || ((this.currentStatus !== 'waiting_approval') && !modal)) return;
|
|
2768
|
-
this.clearIdleFinishCandidate('resolve_modal');
|
|
2769
|
-
this.recordTrace('resolve_modal', {
|
|
2770
|
-
buttonIndex,
|
|
2771
|
-
activeModal: modal,
|
|
2772
|
-
});
|
|
2773
|
-
this.activeModal = null;
|
|
2774
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
2775
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2776
|
-
if (this.approvalExitTimeout) {
|
|
2777
|
-
clearTimeout(this.approvalExitTimeout);
|
|
2778
|
-
this.approvalExitTimeout = null;
|
|
2779
|
-
}
|
|
2780
|
-
this.setStatus('generating', 'approval_resolved');
|
|
2781
|
-
this.onStatusChange?.();
|
|
2782
|
-
if (buttonIndex in this.approvalKeys) {
|
|
2783
|
-
this.ptyProcess.write(this.approvalKeys[buttonIndex]);
|
|
2784
|
-
} else {
|
|
2785
|
-
const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
|
|
2786
|
-
const clampedIndex = buttonCount > 0
|
|
2787
|
-
? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
|
|
2788
|
-
: Math.max(0, buttonIndex);
|
|
2789
|
-
const DOWN = '\x1B[B';
|
|
2790
|
-
const keys = DOWN.repeat(clampedIndex) + '\r';
|
|
2791
|
-
this.ptyProcess.write(keys);
|
|
2792
|
-
}
|
|
1752
|
+
this.engine.resolveModal(buttonIndex);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
1756
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
2793
1757
|
}
|
|
2794
1758
|
|
|
2795
1759
|
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
2796
1760
|
isApprovalRecentlyResolved(): boolean {
|
|
2797
|
-
return
|
|
1761
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2798
1762
|
}
|
|
2799
1763
|
|
|
2800
1764
|
resize(cols: number, rows: number): void {
|
|
@@ -2808,7 +1772,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2808
1772
|
}
|
|
2809
1773
|
|
|
2810
1774
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2811
|
-
if (this.startupParseGate ||
|
|
1775
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2812
1776
|
try {
|
|
2813
1777
|
const parsed = this.getScriptParsedStatus();
|
|
2814
1778
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2826,6 +1790,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2826
1790
|
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2827
1791
|
const parsedDebugState = this.getParsedDebugState();
|
|
2828
1792
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
1793
|
+
const hasFinalAssistant = (p: any) => {
|
|
1794
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1795
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1796
|
+
};
|
|
2829
1797
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
2830
1798
|
if (parsedDebugState?.status === 'error') {
|
|
2831
1799
|
effectiveStatus = 'error';
|
|
@@ -2836,7 +1804,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2836
1804
|
if (
|
|
2837
1805
|
effectiveStatus === 'idle'
|
|
2838
1806
|
&& parsedDebugState?.status === 'generating'
|
|
2839
|
-
&& !
|
|
1807
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
2840
1808
|
) {
|
|
2841
1809
|
effectiveStatus = 'generating';
|
|
2842
1810
|
}
|
|
@@ -2846,8 +1814,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2846
1814
|
providerResolution: this.providerResolutionMeta,
|
|
2847
1815
|
status: effectiveStatus,
|
|
2848
1816
|
projectedStatus: effectiveStatus,
|
|
2849
|
-
rawStatus: this.currentStatus,
|
|
2850
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1817
|
+
rawStatus: this.engine.currentStatus,
|
|
1818
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2851
1819
|
ready: effectiveReady,
|
|
2852
1820
|
startupParseGate: this.startupParseGate,
|
|
2853
1821
|
spawnAt: this.spawnAt,
|
|
@@ -2867,10 +1835,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2867
1835
|
messageCount: parsedMessages.length,
|
|
2868
1836
|
} : null,
|
|
2869
1837
|
screenText: screenText.slice(-4000),
|
|
2870
|
-
currentTurnScope: this.currentTurnScope,
|
|
1838
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2871
1839
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2872
1840
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2873
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2874
1841
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2875
1842
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2876
1843
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
@@ -2888,21 +1855,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2888
1855
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2889
1856
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2890
1857
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2891
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2892
|
-
activeModal: startupModal || this.activeModal,
|
|
2893
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1858
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1859
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
1860
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2894
1861
|
sendDelayMs: this.sendDelayMs,
|
|
2895
1862
|
sendKey: this.sendKey,
|
|
2896
1863
|
submitStrategy: this.submitStrategy,
|
|
2897
1864
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2898
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2899
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1865
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1866
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2900
1867
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2901
1868
|
hasCliScripts: this.hasCliScripts(),
|
|
2902
|
-
scriptNames:
|
|
2903
|
-
traceSessionId: this.
|
|
2904
|
-
traceEntryCount: this.
|
|
2905
|
-
statusHistory: this.
|
|
1869
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1870
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1871
|
+
traceEntryCount: this.engine.getTraceEntries().length,
|
|
1872
|
+
statusHistory: this.engine.getStatusHistory().slice(-30),
|
|
2906
1873
|
timeouts: this.timeouts,
|
|
2907
1874
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2908
1875
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2912,20 +1879,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2912
1879
|
|
|
2913
1880
|
getTraceState(limit = 120): Record<string, any> {
|
|
2914
1881
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
1882
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2915
1883
|
return {
|
|
2916
|
-
sessionId: this.
|
|
1884
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2917
1885
|
providerResolution: this.providerResolutionMeta,
|
|
2918
|
-
entryCount:
|
|
2919
|
-
entries:
|
|
1886
|
+
entryCount: traceEntries.length,
|
|
1887
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2920
1888
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2921
1889
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2922
1890
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2923
1891
|
status: this.projectEffectiveStatus(),
|
|
2924
1892
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2925
|
-
rawStatus: this.currentStatus,
|
|
2926
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2927
|
-
activeModal: this.activeModal,
|
|
2928
|
-
currentTurnScope: this.currentTurnScope,
|
|
1893
|
+
rawStatus: this.engine.currentStatus,
|
|
1894
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1895
|
+
activeModal: this.engine.activeModal,
|
|
1896
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2929
1897
|
messages: [],
|
|
2930
1898
|
};
|
|
2931
1899
|
}
|