@adhdev/daemon-core 0.9.82-rc.135 → 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-adapter-types.d.ts +1 -0
- 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 +75 -74
- package/dist/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +6 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2624 -1946
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2627 -1954
- 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/providers/approval-utils.d.ts +9 -0
- package/dist/repo-mesh-types.d.ts +5 -0
- package/package.json +1 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +1 -0
- 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 +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +377 -1387
- package/src/cli-adapters/provider-cli-parse.ts +6 -0
- package/src/cli-adapters/provider-cli-shared.ts +6 -0
- package/src/commands/chat-commands.ts +23 -1
- package/src/commands/cli-manager.ts +3 -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/approval-utils.ts +27 -0
- package/src/providers/cli-provider-instance.ts +26 -2
- 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,793 +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 = '';
|
|
741
|
+
this.engine.clearAllTimers();
|
|
966
742
|
}
|
|
967
743
|
|
|
968
|
-
|
|
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
|
-
}
|
|
744
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
1019
745
|
|
|
1020
|
-
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
private applyHoldGenerating(ctx: SettledEvalContext, recentInteractiveActivity: boolean): void {
|
|
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
|
-
}
|
|
1237
|
-
|
|
1238
|
-
private applyWaitingApproval(ctx: SettledEvalContext): void {
|
|
1239
|
-
const { modal } = ctx;
|
|
1240
|
-
this.clearIdleFinishCandidate('waiting_approval');
|
|
1241
|
-
const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
|
|
1242
|
-
if (inCooldown && !modal) {
|
|
1243
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1244
|
-
this.activeModal = null;
|
|
1245
|
-
if (this.isWaitingForResponse) {
|
|
1246
|
-
this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
|
|
1247
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1248
|
-
this.idleTimeout = setTimeout(() => {
|
|
1249
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1250
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1251
|
-
this.finishResponse();
|
|
1252
|
-
}
|
|
1253
|
-
}, this.timeouts.generatingIdle);
|
|
1254
|
-
} else {
|
|
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
|
-
baseMessages: [],
|
|
1673
|
-
partialResponse: this.responseBuffer,
|
|
1674
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1675
|
-
scope: this.currentTurnScope,
|
|
1676
|
-
runtimeSettings: this.runtimeSettings,
|
|
1677
|
-
});
|
|
1678
|
-
const session = this.invokeCliScript<ParsedSession | null>(
|
|
1679
|
-
this.cliScripts.parseSession,
|
|
1680
|
-
{ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
|
|
1681
|
-
);
|
|
1682
|
-
this.parseErrorMessage = null;
|
|
1683
|
-
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
1684
|
-
return session && typeof session === 'object' ? session : null;
|
|
1685
|
-
} catch (e: any) {
|
|
1686
|
-
const message = e?.message || String(e);
|
|
1687
|
-
this.parseErrorMessage = message;
|
|
1688
|
-
LOG.warn('CLI', `[${this.cliType}] parseSession error: ${message}`);
|
|
1689
|
-
return null;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
private runDetectStatus(text: string): string | null {
|
|
1694
|
-
if (!this.cliScripts?.detectStatus) return null;
|
|
1695
|
-
try {
|
|
1696
|
-
const screenText = this.terminalScreen.getText();
|
|
1697
|
-
const status = this.invokeCliScript<string | null>(this.cliScripts.detectStatus, {
|
|
1698
|
-
tail: text.slice(-500),
|
|
1699
|
-
screenText,
|
|
1700
|
-
rawBuffer: this.accumulatedRawBuffer,
|
|
1701
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1702
|
-
screen: buildCliScreenSnapshot(screenText),
|
|
1703
|
-
tailScreen: buildCliScreenSnapshot(text.slice(-500)),
|
|
1704
|
-
});
|
|
1705
|
-
return status;
|
|
1706
|
-
} catch (e: any) {
|
|
1707
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
|
|
1708
|
-
return null;
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
1713
|
-
if (!this.cliScripts?.parseApproval) return null;
|
|
1714
|
-
try {
|
|
1715
|
-
const screenText = this.terminalScreen.getText();
|
|
1716
|
-
const buffer = screenText || this.accumulatedBuffer;
|
|
1717
|
-
return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
|
|
1718
|
-
buffer,
|
|
1719
|
-
screenText,
|
|
1720
|
-
rawBuffer: this.accumulatedRawBuffer,
|
|
1721
|
-
tail,
|
|
1722
|
-
screen: buildCliScreenSnapshot(screenText),
|
|
1723
|
-
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
1724
|
-
tailScreen: buildCliScreenSnapshot(tail),
|
|
1725
|
-
});
|
|
1726
|
-
} catch (e: any) {
|
|
1727
|
-
LOG.warn('CLI', `[${this.cliType}] parseApproval error: ${e.message}`);
|
|
1728
|
-
return null;
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
|
|
1733
|
-
return !!(startupModal || this.activeModal);
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
|
|
1737
|
-
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1738
|
-
const lastAssistant = [...messages].reverse().find((message: any) => {
|
|
1739
|
-
if (!message || message.role !== 'assistant') return false;
|
|
1740
|
-
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),
|
|
1741
797
|
});
|
|
1742
|
-
return !!lastAssistant;
|
|
1743
798
|
}
|
|
1744
799
|
|
|
1745
800
|
private applyParsedSessionMetadata(parsed: any): void {
|
|
@@ -1750,62 +805,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1750
805
|
this.providerSessionId = providerSessionId;
|
|
1751
806
|
this.updateRuntimeMeta({ providerSessionId });
|
|
1752
807
|
}
|
|
1753
|
-
this.providerErrorMessage = typeof parsed?.errorMessage === 'string' && parsed.errorMessage.trim()
|
|
1754
|
-
? parsed.errorMessage.trim()
|
|
1755
|
-
: null;
|
|
1756
|
-
this.providerErrorReason = typeof parsed?.errorReason === 'string' && parsed.errorReason.trim()
|
|
1757
|
-
? parsed.errorReason.trim()
|
|
1758
|
-
: null;
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
private parsedStatusHasFinalStandardAssistantMessage(parsed: any): boolean {
|
|
1762
|
-
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1763
|
-
const lastAssistant = [...messages].reverse().find((message: any) => {
|
|
1764
|
-
if (!message || message.role !== 'assistant') return false;
|
|
1765
|
-
return typeof message.content === 'string' && message.content.trim().length > 0;
|
|
1766
|
-
});
|
|
1767
|
-
if (!lastAssistant) return false;
|
|
1768
|
-
const kind = typeof lastAssistant.kind === 'string' && lastAssistant.kind.trim()
|
|
1769
|
-
? lastAssistant.kind.trim()
|
|
1770
|
-
: 'standard';
|
|
1771
|
-
return kind === 'standard' && lastAssistant.meta?.streaming !== true;
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
private shouldKeepCodexTurnOpenForFinish(parsed: any): boolean {
|
|
1775
|
-
if (this.cliType !== 'codex-cli') return false;
|
|
1776
|
-
if (!this.isWaitingForResponse || !this.currentTurnScope || this.hasActionableApproval()) return false;
|
|
1777
|
-
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status.trim() : '';
|
|
1778
|
-
if (parsedStatus !== 'idle') return true;
|
|
1779
|
-
if (parsed?.activeModal || parsed?.modal) return true;
|
|
1780
|
-
return !this.parsedStatusHasFinalStandardAssistantMessage(parsed);
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
private rescheduleCodexFinishCheck(reason: string): void {
|
|
1784
|
-
this.clearIdleFinishCandidate(reason);
|
|
1785
|
-
this.setStatus('generating', reason);
|
|
1786
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1787
|
-
this.idleTimeout = setTimeout(() => {
|
|
1788
|
-
if (!this.isWaitingForResponse || this.hasActionableApproval()) return;
|
|
1789
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1790
|
-
this.evaluateSettled();
|
|
1791
|
-
}, this.getIdleFinishConfirmMs());
|
|
1792
|
-
this.recordTrace('codex_finish_deferred', {
|
|
1793
|
-
reason,
|
|
1794
|
-
...buildCliTraceParseSnapshot({
|
|
1795
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1796
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1797
|
-
responseBuffer: this.responseBuffer,
|
|
1798
|
-
partialResponse: this.responseBuffer,
|
|
1799
|
-
scope: this.currentTurnScope,
|
|
1800
|
-
}),
|
|
1801
|
-
});
|
|
1802
808
|
}
|
|
1803
809
|
|
|
1804
810
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1805
811
|
if (this.parseErrorMessage) return 'error';
|
|
1806
|
-
if (this.
|
|
1807
|
-
if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
|
|
1808
|
-
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;
|
|
1809
815
|
}
|
|
1810
816
|
|
|
1811
817
|
// ─── Public API (CliAdapter) ───────────────────
|
|
@@ -1817,7 +823,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1817
823
|
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
1818
824
|
: null;
|
|
1819
825
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1820
|
-
let effectiveModal = startupModal || this.activeModal;
|
|
826
|
+
let effectiveModal = startupModal || this.engine.activeModal;
|
|
1821
827
|
if (startupDetectedStatus === 'waiting_approval') {
|
|
1822
828
|
effectiveStatus = 'waiting_approval';
|
|
1823
829
|
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
@@ -1829,19 +835,23 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1829
835
|
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
1830
836
|
? parsed.activeModal
|
|
1831
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
|
+
};
|
|
1832
842
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1833
843
|
effectiveStatus = 'waiting_approval';
|
|
1834
844
|
effectiveModal = parsedModal;
|
|
1835
845
|
} else if (
|
|
1836
846
|
effectiveStatus === 'idle'
|
|
1837
847
|
&& parsed?.status === 'generating'
|
|
1838
|
-
&& !
|
|
848
|
+
&& !hasFinalAssistant(parsed)
|
|
1839
849
|
) {
|
|
1840
850
|
effectiveStatus = 'generating';
|
|
1841
851
|
} else if (
|
|
1842
852
|
effectiveStatus === 'generating'
|
|
1843
853
|
&& parsed?.status === 'idle'
|
|
1844
|
-
&&
|
|
854
|
+
&& hasFinalAssistant(parsed)
|
|
1845
855
|
) {
|
|
1846
856
|
effectiveStatus = 'idle';
|
|
1847
857
|
}
|
|
@@ -1860,8 +870,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1860
870
|
queuedAt: message.queuedAt,
|
|
1861
871
|
source: message.source,
|
|
1862
872
|
})),
|
|
1863
|
-
errorMessage: this.parseErrorMessage || this.providerErrorMessage || undefined,
|
|
1864
|
-
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),
|
|
1865
875
|
providerSessionId: this.providerSessionId || undefined,
|
|
1866
876
|
...(bufferState ? { bufferState } : {}),
|
|
1867
877
|
};
|
|
@@ -1881,13 +891,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1881
891
|
!this.providerOwnsTranscript()
|
|
1882
892
|
&& cached
|
|
1883
893
|
&& cached.responseBuffer === this.responseBuffer
|
|
1884
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
894
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1885
895
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1886
896
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1887
897
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1888
898
|
&& cached.screenText === parseScreenText
|
|
1889
|
-
&& cached.currentStatus === this.currentStatus
|
|
1890
|
-
&& cached.activeModal === this.activeModal
|
|
899
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
900
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1891
901
|
&& cached.cliName === this.cliName
|
|
1892
902
|
) {
|
|
1893
903
|
return cached.result;
|
|
@@ -1902,7 +912,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1902
912
|
const bufferState = this.getBufferState();
|
|
1903
913
|
const result = {
|
|
1904
914
|
id: (parsed as any).id || 'cli_session',
|
|
1905
|
-
status: parsed.status || this.currentStatus,
|
|
915
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1906
916
|
title: (parsed as any).title || this.cliName,
|
|
1907
917
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1908
918
|
scope: null,
|
|
@@ -1927,13 +937,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1927
937
|
|
|
1928
938
|
this.parsedStatusCache = {
|
|
1929
939
|
responseBuffer: this.responseBuffer,
|
|
1930
|
-
currentTurnScope: this.currentTurnScope,
|
|
940
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1931
941
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1932
942
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1933
943
|
accumulatedRawBufferKey,
|
|
1934
944
|
screenText: parseScreenText,
|
|
1935
|
-
currentStatus: this.currentStatus,
|
|
1936
|
-
activeModal: this.activeModal,
|
|
945
|
+
currentStatus: this.engine.currentStatus,
|
|
946
|
+
activeModal: this.engine.activeModal,
|
|
1937
947
|
cliName: this.cliName,
|
|
1938
948
|
result,
|
|
1939
949
|
};
|
|
@@ -1941,23 +951,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1941
951
|
}
|
|
1942
952
|
|
|
1943
953
|
async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
|
|
1944
|
-
const fn = this.cliScripts?.[scriptName];
|
|
1945
|
-
if (typeof fn !== 'function') {
|
|
1946
|
-
throw new Error(`CLI script '${scriptName}' not available`);
|
|
1947
|
-
}
|
|
1948
954
|
const input = buildCliParseInput({
|
|
1949
955
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1950
956
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1951
957
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1952
958
|
terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
|
|
1953
959
|
workingDir: this.workingDir,
|
|
960
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
961
|
+
historySessionId: this.providerSessionId || undefined,
|
|
1954
962
|
baseMessages: [],
|
|
1955
963
|
partialResponse: this.responseBuffer,
|
|
1956
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1957
|
-
scope: this.currentTurnScope,
|
|
964
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
965
|
+
scope: this.engine.currentTurnScope,
|
|
1958
966
|
runtimeSettings: this.runtimeSettings,
|
|
1959
967
|
});
|
|
1960
|
-
return await Promise.resolve(this.
|
|
968
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1961
969
|
...input,
|
|
1962
970
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1963
971
|
}));
|
|
@@ -1969,7 +977,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1969
977
|
|
|
1970
978
|
/** Whether this adapter has CLI scripts loaded */
|
|
1971
979
|
hasCliScripts(): boolean {
|
|
1972
|
-
return
|
|
980
|
+
return this.runner.hasDetectStatus();
|
|
1973
981
|
}
|
|
1974
982
|
|
|
1975
983
|
/**
|
|
@@ -1978,24 +986,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1978
986
|
*/
|
|
1979
987
|
async resolveAction(data: any): Promise<void> {
|
|
1980
988
|
let promptText = '';
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
}
|
|
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;
|
|
1987
994
|
}
|
|
1988
995
|
if (!promptText) {
|
|
1989
|
-
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`);
|
|
1990
997
|
return;
|
|
1991
998
|
}
|
|
1992
999
|
await this.sendMessage(promptText);
|
|
1993
1000
|
}
|
|
1994
1001
|
|
|
1995
1002
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
1996
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
1997
|
-
if (this.hasActionableApproval()) return false;
|
|
1998
|
-
|
|
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;
|
|
1999
1007
|
const screenText = this.terminalScreen.getText();
|
|
2000
1008
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
2001
1009
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -2004,20 +1012,41 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2004
1012
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
2005
1013
|
}
|
|
2006
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
|
+
|
|
2007
1040
|
private async writeToPty(data: string): Promise<void> {
|
|
2008
1041
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2009
1042
|
await this.ptyProcess.write(data);
|
|
2010
1043
|
}
|
|
2011
1044
|
|
|
2012
1045
|
private resetPendingSendState(reason: string): void {
|
|
2013
|
-
this.isWaitingForResponse = false;
|
|
2014
1046
|
this.responseBuffer = '';
|
|
2015
|
-
this.currentTurnScope = null;
|
|
2016
|
-
this.submitPendingUntil = 0;
|
|
2017
|
-
this.clearIdleFinishCandidate(reason);
|
|
2018
1047
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
2019
|
-
|
|
2020
|
-
|
|
1048
|
+
this.engine.resetActiveTurnState();
|
|
1049
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
2021
1050
|
}
|
|
2022
1051
|
|
|
2023
1052
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
@@ -2034,42 +1063,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2034
1063
|
}
|
|
2035
1064
|
this.responseTimeout = setTimeout(() => {
|
|
2036
1065
|
this.responseTimeout = null;
|
|
2037
|
-
if (!this.isWaitingForResponse) return;
|
|
2038
|
-
|
|
2039
|
-
const detectedStatusBeforeEval = this.runDetectStatus(this.recentOutputBuffer);
|
|
2040
|
-
this.recordTrace('response_timeout_check', {
|
|
2041
|
-
timeoutMs,
|
|
2042
|
-
detectedStatus: detectedStatusBeforeEval,
|
|
2043
|
-
currentStatus: this.currentStatus,
|
|
2044
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2045
|
-
hasActionableApproval: this.hasActionableApproval(),
|
|
2046
|
-
...buildCliTraceParseSnapshot({
|
|
2047
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
2048
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
2049
|
-
responseBuffer: this.responseBuffer,
|
|
2050
|
-
partialResponse: this.responseBuffer,
|
|
2051
|
-
scope: this.currentTurnScope,
|
|
2052
|
-
}),
|
|
2053
|
-
});
|
|
1066
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
2054
1067
|
|
|
2055
|
-
// maxResponse is a watchdog/checkpoint, not a completion signal.
|
|
2056
|
-
// behavior called finishResponse() unconditionally at the default 300s,
|
|
2057
|
-
// which fabricated idle transitions and downstream generating_completed
|
|
2058
|
-
// notifications while long-running CLIs were still generating. Re-run the
|
|
1068
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
2059
1069
|
// normal settled parser instead and keep the turn open unless the provider
|
|
2060
1070
|
// actually reports an idle, commit-ready state.
|
|
2061
|
-
this.
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
2065
|
-
const detectedStatusAfterEval = this.runDetectStatus(this.recentOutputBuffer);
|
|
2066
|
-
this.recordTrace('response_timeout_kept_open', {
|
|
2067
|
-
timeoutMs,
|
|
2068
|
-
detectedStatusBeforeEval,
|
|
2069
|
-
detectedStatusAfterEval,
|
|
2070
|
-
currentStatus: this.currentStatus,
|
|
2071
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2072
|
-
});
|
|
1071
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1072
|
+
|
|
1073
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
2073
1074
|
this.armResponseTimeout();
|
|
2074
1075
|
}
|
|
2075
1076
|
}, timeoutMs);
|
|
@@ -2084,34 +1085,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2084
1085
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
2085
1086
|
this.submitRetryTimer = null;
|
|
2086
1087
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
2087
|
-
|
|
2088
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1088
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2089
1089
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
|
|
2090
|
-
this.recordTrace('submit_write', {
|
|
2091
|
-
mode: 'submit_retry',
|
|
2092
|
-
attempt,
|
|
2093
|
-
sendKey: this.sendKey,
|
|
2094
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2095
|
-
});
|
|
2096
1090
|
this.writeSubmitKeyForRetry('submit_retry');
|
|
2097
|
-
if (attempt >= 3) { this.submitRetryUsed = true; return; }
|
|
1091
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
2098
1092
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
2099
1093
|
}
|
|
2100
1094
|
|
|
2101
1095
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
2102
1096
|
this.submitRetryTimer = null;
|
|
2103
1097
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
2104
|
-
|
|
2105
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1098
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2106
1099
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
|
|
2107
|
-
this.recordTrace('submit_write', {
|
|
2108
|
-
mode: 'immediate_retry',
|
|
2109
|
-
attempt: 1,
|
|
2110
|
-
sendKey: this.sendKey,
|
|
2111
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2112
|
-
});
|
|
2113
1100
|
this.writeSubmitKeyForRetry('immediate_retry');
|
|
2114
|
-
this.submitRetryUsed = true;
|
|
1101
|
+
this.engine.submitRetryUsed = true;
|
|
2115
1102
|
}
|
|
2116
1103
|
|
|
2117
1104
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -2119,13 +1106,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2119
1106
|
completion.resolveOnce();
|
|
2120
1107
|
return;
|
|
2121
1108
|
}
|
|
2122
|
-
this.submitPendingUntil = 0;
|
|
2123
|
-
const screenText = this.terminalScreen.getText();
|
|
2124
|
-
this.recordTrace('submit_write', {
|
|
2125
|
-
mode: 'submit_key',
|
|
2126
|
-
sendKey: this.sendKey,
|
|
2127
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2128
|
-
});
|
|
1109
|
+
this.engine.submitPendingUntil = 0;
|
|
2129
1110
|
void this.writeToPty(this.sendKey).then(() => {
|
|
2130
1111
|
this.commitSendUserTurn(state);
|
|
2131
1112
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -2135,13 +1116,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2135
1116
|
}
|
|
2136
1117
|
|
|
2137
1118
|
private submitImmediatePrompt(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
2138
|
-
this.submitPendingUntil = 0;
|
|
2139
|
-
this.recordTrace('submit_write', {
|
|
2140
|
-
mode: 'immediate',
|
|
2141
|
-
text: summarizeCliTraceText(state.text, 500),
|
|
2142
|
-
sendKey: this.sendKey,
|
|
2143
|
-
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
|
|
2144
|
-
});
|
|
1119
|
+
this.engine.submitPendingUntil = 0;
|
|
2145
1120
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
2146
1121
|
this.commitSendUserTurn(state);
|
|
2147
1122
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -2185,7 +1160,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2185
1160
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2186
1161
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
2187
1162
|
};
|
|
2188
|
-
|
|
1163
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1164
|
+
|
|
2189
1165
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
2190
1166
|
// At this point the prompt text write already completed. Rejecting without
|
|
2191
1167
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -2222,13 +1198,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2222
1198
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2223
1199
|
const content = String(text || '');
|
|
2224
1200
|
if (!content.trim()) return;
|
|
2225
|
-
|
|
2226
|
-
text: summarizeCliTraceText(content, 500),
|
|
2227
|
-
status: this.currentStatus,
|
|
2228
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2229
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2230
|
-
});
|
|
2231
|
-
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}`);
|
|
2232
1202
|
await this.writeToPty(content + this.sendKey);
|
|
2233
1203
|
this.onStatusChange?.();
|
|
2234
1204
|
}
|
|
@@ -2237,11 +1207,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2237
1207
|
const content = String(text || '');
|
|
2238
1208
|
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
2239
1209
|
if (duplicate) {
|
|
2240
|
-
this.recordTrace('send_message_queued_duplicate_suppressed', {
|
|
2241
|
-
reason,
|
|
2242
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2243
|
-
text: summarizeCliTraceText(content, 500),
|
|
2244
|
-
});
|
|
2245
1210
|
return;
|
|
2246
1211
|
}
|
|
2247
1212
|
const queuedAt = Date.now();
|
|
@@ -2253,24 +1218,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2253
1218
|
source: 'sendMessage',
|
|
2254
1219
|
};
|
|
2255
1220
|
this.pendingOutboundQueue.push(message);
|
|
2256
|
-
this.recordTrace('send_message_queued', {
|
|
2257
|
-
reason,
|
|
2258
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2259
|
-
queuedAt,
|
|
2260
|
-
text: summarizeCliTraceText(content, 500),
|
|
2261
|
-
});
|
|
2262
1221
|
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
2263
1222
|
this.onStatusChange?.();
|
|
2264
1223
|
}
|
|
2265
1224
|
|
|
2266
1225
|
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
2267
1226
|
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
2268
|
-
if (this.hasActionableApproval()) return null;
|
|
1227
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
2269
1228
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
2270
1229
|
? String(parsedStatusBeforeSend.status)
|
|
2271
1230
|
: '';
|
|
2272
|
-
|
|
2273
|
-
|
|
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';
|
|
2274
1237
|
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
|
|
2275
1238
|
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
2276
1239
|
const parsedHasActionableModal = Boolean(
|
|
@@ -2278,15 +1241,15 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2278
1241
|
&& Array.isArray(parsedModal.buttons)
|
|
2279
1242
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
2280
1243
|
);
|
|
2281
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1244
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
2282
1245
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
2283
|
-
&& !this.isWaitingForResponse
|
|
2284
|
-
&& !this.currentTurnScope
|
|
2285
|
-
&& !this.hasActionableApproval()
|
|
1246
|
+
&& !this.engine.isWaitingForResponse
|
|
1247
|
+
&& !this.engine.currentTurnScope
|
|
1248
|
+
&& !this.engine.hasActionableApproval()
|
|
2286
1249
|
&& !parsedHasActionableModal;
|
|
2287
1250
|
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
2288
1251
|
}
|
|
2289
|
-
if (this.isWaitingForResponse && this.currentTurnScope) return 'active_turn_in_progress';
|
|
1252
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
2290
1253
|
return null;
|
|
2291
1254
|
}
|
|
2292
1255
|
|
|
@@ -2300,18 +1263,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2300
1263
|
|
|
2301
1264
|
private async flushPendingOutboundQueue(): Promise<void> {
|
|
2302
1265
|
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
2303
|
-
if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) return;
|
|
1266
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
2304
1267
|
this.pendingOutboundFlushInFlight = true;
|
|
2305
1268
|
try {
|
|
2306
1269
|
while (this.pendingOutboundQueue.length > 0) {
|
|
2307
|
-
if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) break;
|
|
1270
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
2308
1271
|
const next = this.pendingOutboundQueue[0];
|
|
2309
|
-
this.recordTrace('send_message_queue_flush', {
|
|
2310
|
-
id: next.id,
|
|
2311
|
-
queuedAt: next.queuedAt,
|
|
2312
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2313
|
-
text: summarizeCliTraceText(next.content, 500),
|
|
2314
|
-
});
|
|
2315
1272
|
try {
|
|
2316
1273
|
await this.sendMessageNow(next.content, false);
|
|
2317
1274
|
this.pendingOutboundQueue.shift();
|
|
@@ -2331,8 +1288,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2331
1288
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2332
1289
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
2333
1290
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
2334
|
-
&& this.isWaitingForResponse
|
|
2335
|
-
&& !this.hasActionableApproval();
|
|
1291
|
+
&& this.engine.isWaitingForResponse
|
|
1292
|
+
&& !this.engine.hasActionableApproval();
|
|
2336
1293
|
if (this.startupParseGate) {
|
|
2337
1294
|
const deadline = Date.now() + 10000;
|
|
2338
1295
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -2362,11 +1319,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2362
1319
|
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
2363
1320
|
this.ready = true;
|
|
2364
1321
|
this.startupParseGate = false;
|
|
2365
|
-
this.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1322
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
2366
1323
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
2367
1324
|
}
|
|
2368
1325
|
}
|
|
2369
|
-
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})`);
|
|
2370
1327
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
2371
1328
|
? String(parsedStatusBeforeSend.status)
|
|
2372
1329
|
: '';
|
|
@@ -2377,11 +1334,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2377
1334
|
&& Array.isArray(parsedModal.buttons)
|
|
2378
1335
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
2379
1336
|
);
|
|
2380
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1337
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
2381
1338
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
2382
|
-
&& !this.isWaitingForResponse
|
|
2383
|
-
&& !this.currentTurnScope
|
|
2384
|
-
&& !this.hasActionableApproval()
|
|
1339
|
+
&& !this.engine.isWaitingForResponse
|
|
1340
|
+
&& !this.engine.currentTurnScope
|
|
1341
|
+
&& !this.engine.hasActionableApproval()
|
|
2385
1342
|
&& !parsedHasActionableModal;
|
|
2386
1343
|
if (!terminalLooksIdle) {
|
|
2387
1344
|
if (allowQueue) {
|
|
@@ -2391,10 +1348,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2391
1348
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
2392
1349
|
}
|
|
2393
1350
|
}
|
|
2394
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1351
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1352
|
+
const snap = this.getSnapshot();
|
|
2395
1353
|
if (
|
|
2396
|
-
!this.clearStaleIdleResponseGuard('send_message_guard')
|
|
2397
|
-
&& !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)
|
|
2398
1356
|
) {
|
|
2399
1357
|
if (allowQueue) {
|
|
2400
1358
|
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
@@ -2403,30 +1361,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2403
1361
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
2404
1362
|
}
|
|
2405
1363
|
}
|
|
2406
|
-
this.isWaitingForResponse = true;
|
|
2407
1364
|
this.responseBuffer = '';
|
|
2408
|
-
|
|
2409
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
2410
|
-
this.clearIdleFinishCandidate('send_message');
|
|
2411
|
-
this.currentTurnScope = {
|
|
1365
|
+
const turnScope: TurnParseScope = {
|
|
2412
1366
|
prompt: text,
|
|
2413
1367
|
startedAt: Date.now(),
|
|
2414
1368
|
bufferStart: this.accumulatedBuffer.length,
|
|
2415
1369
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
2416
1370
|
};
|
|
2417
|
-
|
|
2418
|
-
text: summarizeCliTraceText(text, 500),
|
|
2419
|
-
estimatedLines: estimatePromptDisplayLines(text),
|
|
2420
|
-
turnScope: this.currentTurnScope,
|
|
2421
|
-
});
|
|
2422
|
-
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2423
|
-
this.submitRetryUsed = false;
|
|
2424
|
-
this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
2425
|
-
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)}`);
|
|
2426
1372
|
if (this.submitRetryTimer) {
|
|
2427
1373
|
clearTimeout(this.submitRetryTimer);
|
|
2428
1374
|
this.submitRetryTimer = null;
|
|
2429
1375
|
}
|
|
1376
|
+
this.engine.onTurnStarted(turnScope);
|
|
1377
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1378
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2430
1379
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2431
1380
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2432
1381
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2439,12 +1388,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2439
1388
|
retryDelayMs,
|
|
2440
1389
|
didCommitUserTurn: false,
|
|
2441
1390
|
};
|
|
2442
|
-
|
|
2443
|
-
clearTimeout(this.settleTimer);
|
|
2444
|
-
this.settleTimer = null;
|
|
2445
|
-
}
|
|
2446
|
-
this.responseEpoch += 1;
|
|
2447
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1391
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2448
1392
|
await new Promise<void>((resolve, reject) => {
|
|
2449
1393
|
let resolved = false;
|
|
2450
1394
|
const completion: SendMessageCompletion = {
|
|
@@ -2467,24 +1411,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2467
1411
|
}
|
|
2468
1412
|
|
|
2469
1413
|
if (submitDelayMs > 0) {
|
|
2470
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1414
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2471
1415
|
}
|
|
2472
|
-
this.recordTrace('submit_write', {
|
|
2473
|
-
mode: 'type_then_submit',
|
|
2474
|
-
text: summarizeCliTraceText(text, 500),
|
|
2475
|
-
sendKey: this.sendKey,
|
|
2476
|
-
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
|
|
2477
|
-
});
|
|
2478
1416
|
const submitStartedAt = Date.now();
|
|
2479
1417
|
void this.writeToPty(text).then(
|
|
2480
1418
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2481
1419
|
completion.rejectOnce,
|
|
2482
1420
|
);
|
|
2483
1421
|
});
|
|
1422
|
+
// Schedule settle after successful send
|
|
1423
|
+
this.engine.scheduleSettle();
|
|
2484
1424
|
}
|
|
2485
1425
|
|
|
2486
1426
|
getPartialResponse(): string {
|
|
2487
|
-
if (!this.isWaitingForResponse) return '';
|
|
1427
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2488
1428
|
return this.responseBuffer;
|
|
2489
1429
|
}
|
|
2490
1430
|
|
|
@@ -2497,10 +1437,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2497
1437
|
cliType: this.cliType,
|
|
2498
1438
|
cliName: this.cliName,
|
|
2499
1439
|
workingDir: this.workingDir,
|
|
2500
|
-
currentStatus: this.currentStatus,
|
|
1440
|
+
currentStatus: this.engine.currentStatus,
|
|
2501
1441
|
ready: this.ready,
|
|
2502
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2503
|
-
activeModal: this.activeModal,
|
|
1442
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1443
|
+
activeModal: this.engine.activeModal,
|
|
2504
1444
|
parseErrorMessage: this.parseErrorMessage,
|
|
2505
1445
|
messageCounts: {
|
|
2506
1446
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2526,10 +1466,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2526
1466
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2527
1467
|
},
|
|
2528
1468
|
parser: {
|
|
2529
|
-
scriptNames:
|
|
2530
|
-
traceSessionId: this.
|
|
2531
|
-
traceSeq: this.
|
|
2532
|
-
currentTurnScope: this.currentTurnScope,
|
|
1469
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1470
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1471
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1472
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2533
1473
|
parsedStatusCache: parsedResult
|
|
2534
1474
|
? {
|
|
2535
1475
|
id: parsedResult.id,
|
|
@@ -2542,26 +1482,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2542
1482
|
activeModal: parsedResult.activeModal,
|
|
2543
1483
|
}
|
|
2544
1484
|
: null,
|
|
2545
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2546
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1485
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1486
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2547
1487
|
},
|
|
2548
1488
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2549
|
-
statusHistory: this.
|
|
2550
|
-
traceEntries: this.
|
|
1489
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1490
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2551
1491
|
timing: {
|
|
2552
1492
|
spawnAt: this.spawnAt,
|
|
2553
1493
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2554
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2555
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2556
|
-
responseEpoch: this.responseEpoch,
|
|
1494
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1495
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1496
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2557
1497
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2558
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1498
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2559
1499
|
},
|
|
2560
1500
|
finish: {
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1501
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1502
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1503
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2565
1504
|
},
|
|
2566
1505
|
};
|
|
2567
1506
|
}
|
|
@@ -2572,6 +1511,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2572
1511
|
}
|
|
2573
1512
|
|
|
2574
1513
|
updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
|
|
1514
|
+
const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
|
|
1515
|
+
? meta.providerSessionId.trim()
|
|
1516
|
+
: '';
|
|
1517
|
+
if (nextProviderSessionId) {
|
|
1518
|
+
this.providerSessionId = nextProviderSessionId;
|
|
1519
|
+
}
|
|
2575
1520
|
if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
|
|
2576
1521
|
this.ptyProcess.updateMeta(meta, replace);
|
|
2577
1522
|
}
|
|
@@ -2592,7 +1537,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2592
1537
|
this.timeouts.shutdownGrace,
|
|
2593
1538
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2594
1539
|
);
|
|
2595
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1540
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2596
1541
|
|
|
2597
1542
|
try {
|
|
2598
1543
|
if (wasProcessing) {
|
|
@@ -2630,7 +1575,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2630
1575
|
return new Promise((resolve) => {
|
|
2631
1576
|
const startedAt = Date.now();
|
|
2632
1577
|
const timer = setInterval(() => {
|
|
2633
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
1578
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2634
1579
|
clearInterval(timer);
|
|
2635
1580
|
resolve(true);
|
|
2636
1581
|
return;
|
|
@@ -2644,12 +1589,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2644
1589
|
}
|
|
2645
1590
|
|
|
2646
1591
|
shutdown(): void {
|
|
2647
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
1592
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2648
1593
|
this.clearAllTimers();
|
|
2649
1594
|
this.pendingOutputParseChunks = [];
|
|
2650
1595
|
this.pendingTerminalQueryTail = '';
|
|
2651
1596
|
this.ptyOutputChunks = [];
|
|
2652
|
-
this.finishRetryCount = 0;
|
|
2653
1597
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2654
1598
|
this.pendingOutboundQueue = [];
|
|
2655
1599
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2658,7 +1602,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2658
1602
|
setTimeout(() => {
|
|
2659
1603
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2660
1604
|
this.ptyProcess = null;
|
|
2661
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
1605
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2662
1606
|
this.ready = false;
|
|
2663
1607
|
this.startupParseGate = false;
|
|
2664
1608
|
this.spawnAt = 0;
|
|
@@ -2668,12 +1612,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2668
1612
|
}
|
|
2669
1613
|
|
|
2670
1614
|
detach(): void {
|
|
2671
|
-
this.clearIdleFinishCandidate('detach');
|
|
1615
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2672
1616
|
this.clearAllTimers();
|
|
2673
1617
|
this.pendingOutputParseChunks = [];
|
|
2674
1618
|
this.pendingTerminalQueryTail = '';
|
|
2675
1619
|
this.ptyOutputChunks = [];
|
|
2676
|
-
this.finishRetryCount = 0;
|
|
2677
1620
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2678
1621
|
this.pendingOutboundQueue = [];
|
|
2679
1622
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2694,19 +1637,17 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2694
1637
|
}
|
|
2695
1638
|
|
|
2696
1639
|
clearHistory(): void {
|
|
2697
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
1640
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2698
1641
|
this.accumulatedBuffer = '';
|
|
2699
1642
|
this.accumulatedRawBuffer = '';
|
|
2700
|
-
this.currentTurnScope = null;
|
|
2701
|
-
this.submitRetryUsed = false;
|
|
2702
|
-
this.submitRetryPromptSnippet = '';
|
|
1643
|
+
this.engine.currentTurnScope = null;
|
|
1644
|
+
this.engine.submitRetryUsed = false;
|
|
1645
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2703
1646
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2704
1647
|
this.pendingOutputParseChunks = [];
|
|
2705
1648
|
this.pendingTerminalQueryTail = '';
|
|
2706
1649
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2707
1650
|
this.ptyOutputChunks = [];
|
|
2708
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
2709
|
-
this.finishRetryCount = 0;
|
|
2710
1651
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2711
1652
|
this.pendingOutboundQueue = [];
|
|
2712
1653
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2715,64 +1656,109 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2715
1656
|
this.onStatusChange?.();
|
|
2716
1657
|
}
|
|
2717
1658
|
|
|
2718
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
1659
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2719
1660
|
isReady(): boolean { return this.ready; }
|
|
2720
1661
|
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
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);
|
|
2727
1749
|
}
|
|
2728
1750
|
|
|
2729
1751
|
resolveModal(buttonIndex: number): void {
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
this.activeModal = parsedModal;
|
|
2741
|
-
if (this.currentStatus !== 'waiting_approval') {
|
|
2742
|
-
this.setStatus('waiting_approval', 'resolve_modal_parse');
|
|
2743
|
-
this.onStatusChange?.();
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
} catch {
|
|
2747
|
-
// Ignore parse failures here; resolveModal falls back to current state.
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
if (!this.ptyProcess || ((this.currentStatus !== 'waiting_approval') && !modal)) return;
|
|
2751
|
-
this.clearIdleFinishCandidate('resolve_modal');
|
|
2752
|
-
this.recordTrace('resolve_modal', {
|
|
2753
|
-
buttonIndex,
|
|
2754
|
-
activeModal: modal,
|
|
2755
|
-
});
|
|
2756
|
-
this.activeModal = null;
|
|
2757
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
2758
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2759
|
-
if (this.approvalExitTimeout) {
|
|
2760
|
-
clearTimeout(this.approvalExitTimeout);
|
|
2761
|
-
this.approvalExitTimeout = null;
|
|
2762
|
-
}
|
|
2763
|
-
this.setStatus('generating', 'approval_resolved');
|
|
2764
|
-
this.onStatusChange?.();
|
|
2765
|
-
if (buttonIndex in this.approvalKeys) {
|
|
2766
|
-
this.ptyProcess.write(this.approvalKeys[buttonIndex]);
|
|
2767
|
-
} else {
|
|
2768
|
-
const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
|
|
2769
|
-
const clampedIndex = buttonCount > 0
|
|
2770
|
-
? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
|
|
2771
|
-
: Math.max(0, buttonIndex);
|
|
2772
|
-
const DOWN = '\x1B[B';
|
|
2773
|
-
const keys = DOWN.repeat(clampedIndex) + '\r';
|
|
2774
|
-
this.ptyProcess.write(keys);
|
|
2775
|
-
}
|
|
1752
|
+
this.engine.resolveModal(buttonIndex);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
1756
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
1760
|
+
isApprovalRecentlyResolved(): boolean {
|
|
1761
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2776
1762
|
}
|
|
2777
1763
|
|
|
2778
1764
|
resize(cols: number, rows: number): void {
|
|
@@ -2786,7 +1772,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2786
1772
|
}
|
|
2787
1773
|
|
|
2788
1774
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2789
|
-
if (this.startupParseGate ||
|
|
1775
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2790
1776
|
try {
|
|
2791
1777
|
const parsed = this.getScriptParsedStatus();
|
|
2792
1778
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2804,6 +1790,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2804
1790
|
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2805
1791
|
const parsedDebugState = this.getParsedDebugState();
|
|
2806
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
|
+
};
|
|
2807
1797
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
2808
1798
|
if (parsedDebugState?.status === 'error') {
|
|
2809
1799
|
effectiveStatus = 'error';
|
|
@@ -2814,7 +1804,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2814
1804
|
if (
|
|
2815
1805
|
effectiveStatus === 'idle'
|
|
2816
1806
|
&& parsedDebugState?.status === 'generating'
|
|
2817
|
-
&& !
|
|
1807
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
2818
1808
|
) {
|
|
2819
1809
|
effectiveStatus = 'generating';
|
|
2820
1810
|
}
|
|
@@ -2824,8 +1814,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2824
1814
|
providerResolution: this.providerResolutionMeta,
|
|
2825
1815
|
status: effectiveStatus,
|
|
2826
1816
|
projectedStatus: effectiveStatus,
|
|
2827
|
-
rawStatus: this.currentStatus,
|
|
2828
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1817
|
+
rawStatus: this.engine.currentStatus,
|
|
1818
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2829
1819
|
ready: effectiveReady,
|
|
2830
1820
|
startupParseGate: this.startupParseGate,
|
|
2831
1821
|
spawnAt: this.spawnAt,
|
|
@@ -2845,10 +1835,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2845
1835
|
messageCount: parsedMessages.length,
|
|
2846
1836
|
} : null,
|
|
2847
1837
|
screenText: screenText.slice(-4000),
|
|
2848
|
-
currentTurnScope: this.currentTurnScope,
|
|
1838
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2849
1839
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2850
1840
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2851
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2852
1841
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2853
1842
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2854
1843
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
@@ -2866,21 +1855,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2866
1855
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2867
1856
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2868
1857
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2869
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2870
|
-
activeModal: startupModal || this.activeModal,
|
|
2871
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1858
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1859
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
1860
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2872
1861
|
sendDelayMs: this.sendDelayMs,
|
|
2873
1862
|
sendKey: this.sendKey,
|
|
2874
1863
|
submitStrategy: this.submitStrategy,
|
|
2875
1864
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2876
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2877
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1865
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1866
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2878
1867
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2879
1868
|
hasCliScripts: this.hasCliScripts(),
|
|
2880
|
-
scriptNames:
|
|
2881
|
-
traceSessionId: this.
|
|
2882
|
-
traceEntryCount: this.
|
|
2883
|
-
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),
|
|
2884
1873
|
timeouts: this.timeouts,
|
|
2885
1874
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2886
1875
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2890,20 +1879,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2890
1879
|
|
|
2891
1880
|
getTraceState(limit = 120): Record<string, any> {
|
|
2892
1881
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
1882
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2893
1883
|
return {
|
|
2894
|
-
sessionId: this.
|
|
1884
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2895
1885
|
providerResolution: this.providerResolutionMeta,
|
|
2896
|
-
entryCount:
|
|
2897
|
-
entries:
|
|
1886
|
+
entryCount: traceEntries.length,
|
|
1887
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2898
1888
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2899
1889
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2900
1890
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2901
1891
|
status: this.projectEffectiveStatus(),
|
|
2902
1892
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2903
|
-
rawStatus: this.currentStatus,
|
|
2904
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2905
|
-
activeModal: this.activeModal,
|
|
2906
|
-
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,
|
|
2907
1897
|
messages: [],
|
|
2908
1898
|
};
|
|
2909
1899
|
}
|