@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.138
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/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +169 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +72 -74
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +5 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3507 -2288
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3515 -2301
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +54 -0
- package/dist/mesh/contracts.d.ts +164 -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/contracts.d.ts +19 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/repo-mesh-types.d.ts +5 -0
- package/dist/shared-types.d.ts +7 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +9 -0
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +1054 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
- package/src/cli-adapters/provider-cli-adapter.ts +413 -1399
- package/src/cli-adapters/provider-cli-parse.ts +3 -0
- package/src/cli-adapters/provider-cli-shared.ts +17 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +715 -368
- package/src/commands/router.ts +22 -2
- package/src/config/chat-history.ts +43 -16
- 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/contracts.ts +329 -0
- 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/contracts.ts +19 -0
- package/src/providers/provider-loader.ts +21 -7
- package/src/providers/provider-schema.ts +12 -0
- package/src/providers/read-chat-contract.ts +74 -14
- package/src/providers/transcript-v2.ts +567 -0
- package/src/repo-mesh-types.ts +10 -0
- package/src/shared-types.ts +7 -0
- package/src/status/snapshot.ts +35 -11
- package/src/types.ts +5 -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,25 @@ 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
|
+
// (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
|
|
174
|
+
// ChatSourceMachine holds the lock by state, not by a mutable field on
|
|
175
|
+
// the adapter. Removed entirely; no callers remain after the readChat
|
|
176
|
+
// ladder was replaced.
|
|
211
177
|
|
|
212
|
-
// ───
|
|
213
|
-
private
|
|
214
|
-
/**
|
|
215
|
-
|
|
178
|
+
// ─── Script runner (parsing isolated here, adapter stays as transport) ───
|
|
179
|
+
private readonly runner: CliScriptRunner;
|
|
180
|
+
/** @deprecated use runner.cliScripts for direct script access */
|
|
181
|
+
get cliScripts(): CliScripts { return this.runner.cliScripts; }
|
|
182
|
+
set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
|
|
216
183
|
private runtimeSettings: Record<string, any> = {};
|
|
217
184
|
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
218
185
|
private accumulatedBuffer: string = '';
|
|
@@ -232,10 +199,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
232
199
|
* Hermes turn (tool calls + reasoning + final bubble) without the
|
|
233
200
|
* rolling window pushing the turn's ╭─ opening line out of view. */
|
|
234
201
|
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
202
|
private parsedStatusCache: {
|
|
240
203
|
responseBuffer: string;
|
|
241
204
|
currentTurnScope: TurnParseScope | null;
|
|
@@ -249,11 +212,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
249
212
|
result: any;
|
|
250
213
|
} | null = null;
|
|
251
214
|
private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
252
|
-
private static readonly MAX_TRACE_ENTRIES = 250;
|
|
253
215
|
|
|
254
216
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
255
|
-
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
256
|
-
private static readonly MAX_FINISH_RETRIES = 2;
|
|
257
217
|
|
|
258
218
|
private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
|
|
259
219
|
const build = (droppedChars: number, maxChars: number) => droppedChars > 0
|
|
@@ -329,13 +289,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
329
289
|
if (
|
|
330
290
|
cached
|
|
331
291
|
&& cached.responseBuffer === this.responseBuffer
|
|
332
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
292
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
333
293
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
334
294
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
335
295
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
336
296
|
&& cached.screenText === this.lastScreenText
|
|
337
|
-
&& cached.currentStatus === this.currentStatus
|
|
338
|
-
&& cached.activeModal === this.activeModal
|
|
297
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
298
|
+
&& cached.activeModal === this.engine.activeModal
|
|
339
299
|
&& cached.cliName === this.cliName
|
|
340
300
|
) {
|
|
341
301
|
return cached.result;
|
|
@@ -359,86 +319,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
359
319
|
return this.timeouts.statusActivityHold;
|
|
360
320
|
}
|
|
361
321
|
|
|
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
322
|
// Resolved timeouts
|
|
443
323
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
444
324
|
|
|
@@ -448,7 +328,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
448
328
|
private readonly sendKey: string;
|
|
449
329
|
private readonly submitStrategy: 'wait_for_echo' | 'immediate';
|
|
450
330
|
private readonly requirePromptEchoBeforeSubmit: boolean;
|
|
451
|
-
private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
|
|
452
331
|
|
|
453
332
|
constructor(
|
|
454
333
|
provider: CliProviderModule,
|
|
@@ -457,6 +336,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
457
336
|
private extraEnv: Record<string, string> = {},
|
|
458
337
|
transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
|
|
459
338
|
) {
|
|
339
|
+
this.runner = new CliScriptRunner(provider.type);
|
|
460
340
|
this.provider = provider;
|
|
461
341
|
this.transportFactory = transportFactory;
|
|
462
342
|
this.cliType = provider.type;
|
|
@@ -474,10 +354,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
474
354
|
this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
|
|
475
355
|
this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
|
|
476
356
|
|
|
477
|
-
//
|
|
478
|
-
this.
|
|
479
|
-
|
|
480
|
-
|
|
357
|
+
// State machine engine — owns all status transitions
|
|
358
|
+
this.engine = new CliStateEngine(
|
|
359
|
+
provider,
|
|
360
|
+
this.runner,
|
|
361
|
+
this as unknown as CliTransportAccess,
|
|
362
|
+
{
|
|
363
|
+
onStatusChange: () => { this.onStatusChange?.(); },
|
|
364
|
+
onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
|
|
365
|
+
onTurnCompleted: () => { this.responseBuffer = ''; },
|
|
366
|
+
} satisfies CliStateEngineCallbacks,
|
|
367
|
+
resolvedConfig.timeouts,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Scripts delegated to CliScriptRunner — adapter stays as transport
|
|
371
|
+
this.runner.setScripts(provider.scripts || {});
|
|
372
|
+
const scriptNames = this.runner.getScriptNames();
|
|
481
373
|
if (scriptNames.length > 0) {
|
|
482
374
|
LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
|
|
483
375
|
LOG.info(
|
|
@@ -503,14 +395,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
503
395
|
|
|
504
396
|
/** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
|
|
505
397
|
setCliScripts(scripts: CliScripts): void {
|
|
506
|
-
this.
|
|
398
|
+
this.runner.setScripts(scripts);
|
|
507
399
|
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(', ')}]`);
|
|
400
|
+
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
|
|
514
401
|
}
|
|
515
402
|
|
|
516
403
|
/** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
|
|
@@ -564,15 +451,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
564
451
|
});
|
|
565
452
|
|
|
566
453
|
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
454
|
|
|
577
455
|
try {
|
|
578
456
|
this.ptyProcess = this.transportFactory.spawn(
|
|
@@ -636,13 +514,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
636
514
|
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
637
515
|
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
|
|
638
516
|
this.flushPendingOutputParse();
|
|
639
|
-
this.recordTrace('exit', { exitCode });
|
|
640
517
|
this.ptyProcess = null;
|
|
641
|
-
this.
|
|
518
|
+
this.engine.onPtyExit();
|
|
642
519
|
this.ready = false;
|
|
643
520
|
this.startupParseGate = false;
|
|
644
521
|
this.spawnAt = 0;
|
|
645
|
-
this.
|
|
522
|
+
this.runner.resetSessionState();
|
|
646
523
|
this.onStatusChange?.();
|
|
647
524
|
});
|
|
648
525
|
|
|
@@ -653,15 +530,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
653
530
|
if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
|
|
654
531
|
this.resetTerminalScreen(24, 80);
|
|
655
532
|
this.pendingTerminalQueryTail = '';
|
|
656
|
-
this.currentTurnScope = null;
|
|
657
|
-
this.finishRetryCount = 0;
|
|
658
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
659
533
|
this.ready = false;
|
|
660
534
|
await this.ptyProcess.ready;
|
|
661
|
-
this.
|
|
662
|
-
runtimeMeta: this.getRuntimeMetadata(),
|
|
663
|
-
});
|
|
664
|
-
this.setStatus('starting', 'pty_ready');
|
|
535
|
+
this.engine.onSpawnReady();
|
|
665
536
|
this.scheduleStartupSettleCheck();
|
|
666
537
|
this.onStatusChange?.();
|
|
667
538
|
}
|
|
@@ -687,11 +558,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
687
558
|
if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
|
|
688
559
|
this.startupFirstOutputAt = now;
|
|
689
560
|
}
|
|
690
|
-
if (
|
|
691
|
-
this.clearIdleFinishCandidate('new_output');
|
|
561
|
+
if (rawData.length > 0 || cleanData.length > 0) {
|
|
562
|
+
this.engine.clearIdleFinishCandidate('new_output');
|
|
692
563
|
}
|
|
693
564
|
if (getDebugRuntimeConfig().collectDebugTrace) {
|
|
694
|
-
this.
|
|
565
|
+
this.engine.recordExternalTrace('output', {
|
|
695
566
|
rawLength: rawData.length,
|
|
696
567
|
cleanLength: cleanData.length,
|
|
697
568
|
rawPreview: summarizeCliTraceText(rawData, 300),
|
|
@@ -703,7 +574,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
703
574
|
this.scheduleStartupSettleCheck();
|
|
704
575
|
}
|
|
705
576
|
|
|
706
|
-
if (this.isWaitingForResponse && cleanData) {
|
|
577
|
+
if (this.engine.isWaitingForResponse && cleanData) {
|
|
707
578
|
const previousResponseLen = this.responseBuffer.length;
|
|
708
579
|
this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
|
|
709
580
|
this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
|
|
@@ -742,19 +613,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
742
613
|
// Keep turn-scope offsets aligned with the truncated buffer so scoped
|
|
743
614
|
// parses don't lose the beginning of a long turn (e.g. the Hermes
|
|
744
615
|
// ╭─ opening line) when the rolling window sheds bytes.
|
|
745
|
-
if (this.currentTurnScope) {
|
|
616
|
+
if (this.engine.currentTurnScope) {
|
|
746
617
|
if (droppedClean > 0) {
|
|
747
|
-
this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
|
|
618
|
+
this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
|
|
748
619
|
}
|
|
749
620
|
if (droppedRaw > 0) {
|
|
750
|
-
this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
|
|
621
|
+
this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
|
|
751
622
|
}
|
|
752
623
|
}
|
|
753
624
|
|
|
754
625
|
this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
|
|
755
626
|
|
|
756
627
|
// ─── Script-based status detection
|
|
757
|
-
this.scheduleSettle();
|
|
628
|
+
this.engine.scheduleSettle();
|
|
758
629
|
}
|
|
759
630
|
|
|
760
631
|
private resolveStartupState(
|
|
@@ -779,12 +650,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
779
650
|
const startupModal = this.runParseApproval(this.recentOutputBuffer);
|
|
780
651
|
const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
781
652
|
if (!startupModal && startupStatus !== 'idle') {
|
|
782
|
-
this.recordTrace('startup_settle_deferred', {
|
|
783
|
-
trigger,
|
|
784
|
-
startupStatus,
|
|
785
|
-
stableMs,
|
|
786
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
787
|
-
});
|
|
788
653
|
this.scheduleStartupSettleCheck();
|
|
789
654
|
return;
|
|
790
655
|
}
|
|
@@ -795,14 +660,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
795
660
|
}
|
|
796
661
|
this.ready = true;
|
|
797
662
|
if (startupModal) {
|
|
798
|
-
this.activeModal = startupModal;
|
|
799
|
-
this.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
663
|
+
this.engine.activeModal = startupModal;
|
|
664
|
+
this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
800
665
|
} else {
|
|
801
|
-
if (this.currentStatus === 'waiting_approval' || this.activeModal) {
|
|
802
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
666
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
|
|
667
|
+
this.engine.lastApprovalResolvedAt = Date.now();
|
|
803
668
|
}
|
|
804
|
-
this.activeModal = null;
|
|
805
|
-
|
|
669
|
+
this.engine.activeModal = null;
|
|
670
|
+
// Clear the in-flight turn flag at the same time we declare
|
|
671
|
+
// startup-idle. Otherwise the next settled evaluation sees
|
|
672
|
+
// isWaitingForResponse=true + recent CLI welcome-screen paints
|
|
673
|
+
// and flips us right back to generating via the hold path
|
|
674
|
+
// (the "startup → generating → idle → generating → idle"
|
|
675
|
+
// flicker the user observed on claude-cli launch).
|
|
676
|
+
this.engine.isWaitingForResponse = false;
|
|
677
|
+
this.engine.currentTurnScope = null;
|
|
678
|
+
this.engine.setStatus('idle', `startup_ready:${trigger}`);
|
|
806
679
|
}
|
|
807
680
|
LOG.info(
|
|
808
681
|
'CLI',
|
|
@@ -828,88 +701,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
828
701
|
}, delayMs);
|
|
829
702
|
}
|
|
830
703
|
|
|
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
704
|
private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
|
|
914
705
|
const startedAt = Date.now();
|
|
915
706
|
let loggedWait = false;
|
|
@@ -919,7 +710,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
919
710
|
const screenText = this.terminalScreen.getText() || '';
|
|
920
711
|
const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
|
|
921
712
|
const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
922
|
-
const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
713
|
+
const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
|
|
923
714
|
const interactiveReady = status === 'idle'
|
|
924
715
|
&& stableMs >= 700
|
|
925
716
|
&& recentlyOutput >= 350;
|
|
@@ -953,861 +744,84 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
953
744
|
|
|
954
745
|
private clearAllTimers(): void {
|
|
955
746
|
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
747
|
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
748
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
963
749
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
964
|
-
|
|
965
|
-
this.providerErrorRetryKey = '';
|
|
750
|
+
this.engine.clearAllTimers();
|
|
966
751
|
}
|
|
967
752
|
|
|
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
|
-
}
|
|
753
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
1019
754
|
|
|
1020
|
-
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
}),
|
|
755
|
+
runParseSession(): ParsedSession | null {
|
|
756
|
+
const screenText = this.terminalScreen.getText();
|
|
757
|
+
const parseScreenText = this.getParseScreenText(screenText);
|
|
758
|
+
const tail = this.recentOutputBuffer.slice(-500);
|
|
759
|
+
const input = buildCliParseInput({
|
|
760
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
761
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
762
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
763
|
+
terminalScreenText: parseScreenText,
|
|
764
|
+
workingDir: this.workingDir,
|
|
765
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
766
|
+
historySessionId: this.providerSessionId || undefined,
|
|
767
|
+
baseMessages: [],
|
|
768
|
+
partialResponse: this.responseBuffer,
|
|
769
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
770
|
+
scope: this.engine.currentTurnScope,
|
|
771
|
+
runtimeSettings: this.runtimeSettings,
|
|
772
|
+
spawnAt: this.spawnAt,
|
|
1466
773
|
});
|
|
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
|
-
}),
|
|
774
|
+
const session = this.runner.parseSession({
|
|
775
|
+
...input,
|
|
776
|
+
tail,
|
|
777
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1516
778
|
});
|
|
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();
|
|
779
|
+
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
780
|
+
return session;
|
|
1553
781
|
}
|
|
1554
782
|
|
|
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),
|
|
783
|
+
runDetectStatus(text: string): string | null {
|
|
784
|
+
const screenText = this.terminalScreen.getText();
|
|
785
|
+
const tail = text.slice(-500);
|
|
786
|
+
return this.runner.detectStatus({
|
|
787
|
+
tail,
|
|
788
|
+
screenText,
|
|
789
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
790
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
791
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
792
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1587
793
|
});
|
|
1588
|
-
return true;
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
private commitCurrentTranscript(): { hasAssistant: boolean; assistantContent: string } {
|
|
1592
|
-
const parsed = this.parseCurrentTranscript(
|
|
1593
|
-
[],
|
|
1594
|
-
this.responseBuffer,
|
|
1595
|
-
this.currentTurnScope,
|
|
1596
|
-
);
|
|
1597
|
-
if (parsed && Array.isArray(parsed.messages)) {
|
|
1598
|
-
const parsedMessages = normalizeCliParsedMessages(parsed.messages, {
|
|
1599
|
-
scope: null,
|
|
1600
|
-
lastOutputAt: this.lastOutputAt,
|
|
1601
|
-
});
|
|
1602
|
-
const lastAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
|
|
1603
|
-
if (this.currentTurnScope) {
|
|
1604
|
-
LOG.info(
|
|
1605
|
-
'CLI',
|
|
1606
|
-
`[${this.cliType}] commitCurrentTranscript parserMessages=${parsedMessages.length} finalLastAssistant=${JSON.stringify(summarizeCliTraceText(lastAssistant?.content || '', 220)).slice(0, 260)}`
|
|
1607
|
-
);
|
|
1608
|
-
}
|
|
1609
|
-
this.recordTrace('commit_transcript', {
|
|
1610
|
-
parsedStatus: parsed.status || null,
|
|
1611
|
-
messageCount: parsedMessages.length,
|
|
1612
|
-
lastAssistant: lastAssistant ? summarizeCliTraceText(lastAssistant.content, 320) : '',
|
|
1613
|
-
messages: summarizeCliTraceMessages(parsedMessages),
|
|
1614
|
-
...buildCliTraceParseSnapshot({
|
|
1615
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1616
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1617
|
-
responseBuffer: this.responseBuffer,
|
|
1618
|
-
partialResponse: this.responseBuffer,
|
|
1619
|
-
scope: this.currentTurnScope,
|
|
1620
|
-
}),
|
|
1621
|
-
});
|
|
1622
|
-
if (!lastAssistant && this.currentTurnScope) {
|
|
1623
|
-
LOG.warn(
|
|
1624
|
-
'CLI',
|
|
1625
|
-
`[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
|
|
1626
|
-
);
|
|
1627
|
-
}
|
|
1628
|
-
const hasAssistant = !!lastAssistant;
|
|
1629
|
-
return {
|
|
1630
|
-
hasAssistant,
|
|
1631
|
-
assistantContent: lastAssistant?.content || '',
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
if (this.currentTurnScope) {
|
|
1635
|
-
LOG.info(
|
|
1636
|
-
'CLI',
|
|
1637
|
-
`[${this.cliType}] commitCurrentTranscript parsed.messages=none responseBufferLen=${this.responseBuffer.length} accumulatedBufferLen=${this.accumulatedBuffer.length} parsedStatus=${parsed?.status || '-'} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
|
|
1638
|
-
);
|
|
1639
|
-
}
|
|
1640
|
-
return {
|
|
1641
|
-
hasAssistant: false,
|
|
1642
|
-
assistantContent: '',
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
// ─── Script Execution ──────────────────────────
|
|
1648
|
-
|
|
1649
|
-
private invokeCliScript<T>(script: Function, input: any): T {
|
|
1650
|
-
const hasStateFactory = typeof this.cliScripts?.createState === 'function';
|
|
1651
|
-
const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
|
|
1652
|
-
return expectsStateArgument
|
|
1653
|
-
? script(this.scriptState, input)
|
|
1654
|
-
: script(input);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
private runParseSession(): ParsedSession | null {
|
|
1658
|
-
if (typeof this.cliScripts?.parseSession !== 'function') {
|
|
1659
|
-
this.parseErrorMessage = `${this.cliType} parseSession unavailable`;
|
|
1660
|
-
return null;
|
|
1661
|
-
}
|
|
1662
|
-
try {
|
|
1663
|
-
const screenText = this.terminalScreen.getText();
|
|
1664
|
-
const parseScreenText = this.getParseScreenText(screenText);
|
|
1665
|
-
const tail = this.recentOutputBuffer.slice(-500);
|
|
1666
|
-
const input = buildCliParseInput({
|
|
1667
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1668
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1669
|
-
recentOutputBuffer: this.recentOutputBuffer,
|
|
1670
|
-
terminalScreenText: parseScreenText,
|
|
1671
|
-
workingDir: this.workingDir,
|
|
1672
|
-
providerSessionId: this.providerSessionId || undefined,
|
|
1673
|
-
historySessionId: this.providerSessionId || undefined,
|
|
1674
|
-
baseMessages: [],
|
|
1675
|
-
partialResponse: this.responseBuffer,
|
|
1676
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1677
|
-
scope: this.currentTurnScope,
|
|
1678
|
-
runtimeSettings: this.runtimeSettings,
|
|
1679
|
-
});
|
|
1680
|
-
const session = this.invokeCliScript<ParsedSession | null>(
|
|
1681
|
-
this.cliScripts.parseSession,
|
|
1682
|
-
{ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
|
|
1683
|
-
);
|
|
1684
|
-
this.parseErrorMessage = null;
|
|
1685
|
-
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
1686
|
-
return session && typeof session === 'object' ? session : null;
|
|
1687
|
-
} catch (e: any) {
|
|
1688
|
-
const message = e?.message || String(e);
|
|
1689
|
-
this.parseErrorMessage = message;
|
|
1690
|
-
LOG.warn('CLI', `[${this.cliType}] parseSession error: ${message}`);
|
|
1691
|
-
return null;
|
|
1692
|
-
}
|
|
1693
794
|
}
|
|
1694
795
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
});
|
|
1707
|
-
return status;
|
|
1708
|
-
} catch (e: any) {
|
|
1709
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
|
|
1710
|
-
return null;
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
1715
|
-
if (!this.cliScripts?.parseApproval) return null;
|
|
1716
|
-
try {
|
|
1717
|
-
const screenText = this.terminalScreen.getText();
|
|
1718
|
-
const buffer = screenText || this.accumulatedBuffer;
|
|
1719
|
-
return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
|
|
1720
|
-
buffer,
|
|
1721
|
-
screenText,
|
|
1722
|
-
rawBuffer: this.accumulatedRawBuffer,
|
|
1723
|
-
tail,
|
|
1724
|
-
screen: buildCliScreenSnapshot(screenText),
|
|
1725
|
-
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
1726
|
-
tailScreen: buildCliScreenSnapshot(tail),
|
|
1727
|
-
});
|
|
1728
|
-
} catch (e: any) {
|
|
1729
|
-
LOG.warn('CLI', `[${this.cliType}] parseApproval error: ${e.message}`);
|
|
1730
|
-
return null;
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
|
|
1735
|
-
return !!(startupModal || this.activeModal);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
|
|
1739
|
-
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1740
|
-
const lastAssistant = [...messages].reverse().find((message: any) => {
|
|
1741
|
-
if (!message || message.role !== 'assistant') return false;
|
|
1742
|
-
return typeof message.content === 'string' && message.content.trim().length > 0;
|
|
796
|
+
runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
797
|
+
const screenText = this.terminalScreen.getText();
|
|
798
|
+
const buffer = screenText || this.accumulatedBuffer;
|
|
799
|
+
return this.runner.parseApproval({
|
|
800
|
+
buffer,
|
|
801
|
+
screenText,
|
|
802
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
803
|
+
tail,
|
|
804
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
805
|
+
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
806
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1743
807
|
});
|
|
1744
|
-
return !!lastAssistant;
|
|
1745
808
|
}
|
|
1746
809
|
|
|
1747
810
|
private applyParsedSessionMetadata(parsed: any): void {
|
|
1748
811
|
const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
|
|
1749
812
|
? parsed.providerSessionId.trim()
|
|
1750
813
|
: '';
|
|
1751
|
-
if (providerSessionId) {
|
|
814
|
+
if (providerSessionId && providerSessionId !== this.providerSessionId) {
|
|
1752
815
|
this.providerSessionId = providerSessionId;
|
|
1753
816
|
this.updateRuntimeMeta({ providerSessionId });
|
|
1754
817
|
}
|
|
1755
|
-
this.providerErrorMessage = typeof parsed?.errorMessage === 'string' && parsed.errorMessage.trim()
|
|
1756
|
-
? parsed.errorMessage.trim()
|
|
1757
|
-
: null;
|
|
1758
|
-
this.providerErrorReason = typeof parsed?.errorReason === 'string' && parsed.errorReason.trim()
|
|
1759
|
-
? parsed.errorReason.trim()
|
|
1760
|
-
: null;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
private parsedStatusHasFinalStandardAssistantMessage(parsed: any): boolean {
|
|
1764
|
-
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1765
|
-
const lastAssistant = [...messages].reverse().find((message: any) => {
|
|
1766
|
-
if (!message || message.role !== 'assistant') return false;
|
|
1767
|
-
return typeof message.content === 'string' && message.content.trim().length > 0;
|
|
1768
|
-
});
|
|
1769
|
-
if (!lastAssistant) return false;
|
|
1770
|
-
const kind = typeof lastAssistant.kind === 'string' && lastAssistant.kind.trim()
|
|
1771
|
-
? lastAssistant.kind.trim()
|
|
1772
|
-
: 'standard';
|
|
1773
|
-
return kind === 'standard' && lastAssistant.meta?.streaming !== true;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
private shouldKeepCodexTurnOpenForFinish(parsed: any): boolean {
|
|
1777
|
-
if (this.cliType !== 'codex-cli') return false;
|
|
1778
|
-
if (!this.isWaitingForResponse || !this.currentTurnScope || this.hasActionableApproval()) return false;
|
|
1779
|
-
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status.trim() : '';
|
|
1780
|
-
if (parsedStatus !== 'idle') return true;
|
|
1781
|
-
if (parsed?.activeModal || parsed?.modal) return true;
|
|
1782
|
-
return !this.parsedStatusHasFinalStandardAssistantMessage(parsed);
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
private rescheduleCodexFinishCheck(reason: string): void {
|
|
1786
|
-
this.clearIdleFinishCandidate(reason);
|
|
1787
|
-
this.setStatus('generating', reason);
|
|
1788
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1789
|
-
this.idleTimeout = setTimeout(() => {
|
|
1790
|
-
if (!this.isWaitingForResponse || this.hasActionableApproval()) return;
|
|
1791
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1792
|
-
this.evaluateSettled();
|
|
1793
|
-
}, this.getIdleFinishConfirmMs());
|
|
1794
|
-
this.recordTrace('codex_finish_deferred', {
|
|
1795
|
-
reason,
|
|
1796
|
-
...buildCliTraceParseSnapshot({
|
|
1797
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1798
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1799
|
-
responseBuffer: this.responseBuffer,
|
|
1800
|
-
partialResponse: this.responseBuffer,
|
|
1801
|
-
scope: this.currentTurnScope,
|
|
1802
|
-
}),
|
|
1803
|
-
});
|
|
1804
818
|
}
|
|
1805
819
|
|
|
1806
820
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1807
821
|
if (this.parseErrorMessage) return 'error';
|
|
1808
|
-
if (this.
|
|
1809
|
-
if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
|
|
1810
|
-
return this.currentStatus;
|
|
822
|
+
if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
|
|
823
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
|
|
824
|
+
return this.engine.currentStatus;
|
|
1811
825
|
}
|
|
1812
826
|
|
|
1813
827
|
// ─── Public API (CliAdapter) ───────────────────
|
|
@@ -1819,8 +833,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1819
833
|
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
1820
834
|
: null;
|
|
1821
835
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1822
|
-
let effectiveModal = startupModal || this.activeModal;
|
|
1823
|
-
|
|
836
|
+
let effectiveModal = startupModal || this.engine.activeModal;
|
|
837
|
+
// (fix) When we have no captured modal yet, take one more live attempt
|
|
838
|
+
// with the current screen text — the engine's settle pass can miss
|
|
839
|
+
// the modal when it happens to fire exactly between writes, and
|
|
840
|
+
// without a modal here the dashboard could never show the buttons.
|
|
841
|
+
// We deliberately do NOT overwrite an existing engine.activeModal so
|
|
842
|
+
// a stable matched modal wins. This runs even outside the startup
|
|
843
|
+
// gate because Claude's approval frames can appear long after launch.
|
|
844
|
+
if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
|
|
845
|
+
const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
|
|
846
|
+
if (liveDetect === 'waiting_approval') {
|
|
847
|
+
const liveModal = this.runParseApproval(this.terminalScreen.getText())
|
|
848
|
+
|| this.runParseApproval(this.recentOutputBuffer);
|
|
849
|
+
if (liveModal) {
|
|
850
|
+
effectiveModal = liveModal;
|
|
851
|
+
// Promote so subsequent calls don't re-walk the buffer.
|
|
852
|
+
// Only set if engine hasn't already captured one — keeps
|
|
853
|
+
// the first stable modal as authoritative.
|
|
854
|
+
if (!this.engine.activeModal) this.engine.activeModal = liveModal;
|
|
855
|
+
} else {
|
|
856
|
+
LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: detect=waiting_approval but parseApproval still null (recentLen=${this.recentOutputBuffer.length} screenLen=${this.terminalScreen.getText().length})`);
|
|
857
|
+
}
|
|
858
|
+
} else if (liveDetect && liveDetect !== 'generating' && liveDetect !== 'idle') {
|
|
859
|
+
LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: detect=${liveDetect} (not waiting_approval)`);
|
|
860
|
+
} else if (this.engine.currentStatus === 'waiting_approval' && liveDetect !== 'waiting_approval') {
|
|
861
|
+
LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: engine.status=waiting_approval but live detect=${liveDetect}`);
|
|
862
|
+
}
|
|
863
|
+
} else if (!effectiveModal && this.engine.currentStatus === 'waiting_approval') {
|
|
864
|
+
LOG.warn('CLI', `[${this.cliType}] getStatus skipped live re-extract: allowParse=${allowParse} isWaitingForResponse=${this.engine.isWaitingForResponse}`);
|
|
865
|
+
}
|
|
866
|
+
// Only surface waiting_approval when we ALSO have a concrete modal
|
|
867
|
+
// (message + buttons). detectStatus alone can fire while parseApproval
|
|
868
|
+
// is still null — the engine logs "detectStatus=waiting_approval but
|
|
869
|
+
// parseApproval returned null; ignoring". Without this guard getStatus
|
|
870
|
+
// was shipping a bare waiting_approval with activeModal=null and the
|
|
871
|
+
// user perceived the flow as broken.
|
|
872
|
+
if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
|
|
1824
873
|
effectiveStatus = 'waiting_approval';
|
|
1825
874
|
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
1826
875
|
effectiveStatus = 'idle';
|
|
@@ -1831,19 +880,23 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1831
880
|
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
1832
881
|
? parsed.activeModal
|
|
1833
882
|
: null;
|
|
883
|
+
const hasFinalAssistant = (p: any) => {
|
|
884
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
885
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
886
|
+
};
|
|
1834
887
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1835
888
|
effectiveStatus = 'waiting_approval';
|
|
1836
889
|
effectiveModal = parsedModal;
|
|
1837
890
|
} else if (
|
|
1838
891
|
effectiveStatus === 'idle'
|
|
1839
892
|
&& parsed?.status === 'generating'
|
|
1840
|
-
&& !
|
|
893
|
+
&& !hasFinalAssistant(parsed)
|
|
1841
894
|
) {
|
|
1842
895
|
effectiveStatus = 'generating';
|
|
1843
896
|
} else if (
|
|
1844
897
|
effectiveStatus === 'generating'
|
|
1845
898
|
&& parsed?.status === 'idle'
|
|
1846
|
-
&&
|
|
899
|
+
&& hasFinalAssistant(parsed)
|
|
1847
900
|
) {
|
|
1848
901
|
effectiveStatus = 'idle';
|
|
1849
902
|
}
|
|
@@ -1862,8 +915,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1862
915
|
queuedAt: message.queuedAt,
|
|
1863
916
|
source: message.source,
|
|
1864
917
|
})),
|
|
1865
|
-
errorMessage: this.parseErrorMessage || this.providerErrorMessage || undefined,
|
|
1866
|
-
errorReason: this.parseErrorMessage ? 'parse_error' : (this.providerErrorReason || undefined),
|
|
918
|
+
errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
|
|
919
|
+
errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
|
|
1867
920
|
providerSessionId: this.providerSessionId || undefined,
|
|
1868
921
|
...(bufferState ? { bufferState } : {}),
|
|
1869
922
|
};
|
|
@@ -1883,13 +936,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1883
936
|
!this.providerOwnsTranscript()
|
|
1884
937
|
&& cached
|
|
1885
938
|
&& cached.responseBuffer === this.responseBuffer
|
|
1886
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
939
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1887
940
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1888
941
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1889
942
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1890
943
|
&& cached.screenText === parseScreenText
|
|
1891
|
-
&& cached.currentStatus === this.currentStatus
|
|
1892
|
-
&& cached.activeModal === this.activeModal
|
|
944
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
945
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1893
946
|
&& cached.cliName === this.cliName
|
|
1894
947
|
) {
|
|
1895
948
|
return cached.result;
|
|
@@ -1904,7 +957,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1904
957
|
const bufferState = this.getBufferState();
|
|
1905
958
|
const result = {
|
|
1906
959
|
id: (parsed as any).id || 'cli_session',
|
|
1907
|
-
status: parsed.status || this.currentStatus,
|
|
960
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1908
961
|
title: (parsed as any).title || this.cliName,
|
|
1909
962
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1910
963
|
scope: null,
|
|
@@ -1929,13 +982,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1929
982
|
|
|
1930
983
|
this.parsedStatusCache = {
|
|
1931
984
|
responseBuffer: this.responseBuffer,
|
|
1932
|
-
currentTurnScope: this.currentTurnScope,
|
|
985
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1933
986
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1934
987
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1935
988
|
accumulatedRawBufferKey,
|
|
1936
989
|
screenText: parseScreenText,
|
|
1937
|
-
currentStatus: this.currentStatus,
|
|
1938
|
-
activeModal: this.activeModal,
|
|
990
|
+
currentStatus: this.engine.currentStatus,
|
|
991
|
+
activeModal: this.engine.activeModal,
|
|
1939
992
|
cliName: this.cliName,
|
|
1940
993
|
result,
|
|
1941
994
|
};
|
|
@@ -1943,10 +996,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1943
996
|
}
|
|
1944
997
|
|
|
1945
998
|
async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
|
|
1946
|
-
const fn = this.cliScripts?.[scriptName];
|
|
1947
|
-
if (typeof fn !== 'function') {
|
|
1948
|
-
throw new Error(`CLI script '${scriptName}' not available`);
|
|
1949
|
-
}
|
|
1950
999
|
const input = buildCliParseInput({
|
|
1951
1000
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1952
1001
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
@@ -1957,11 +1006,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1957
1006
|
historySessionId: this.providerSessionId || undefined,
|
|
1958
1007
|
baseMessages: [],
|
|
1959
1008
|
partialResponse: this.responseBuffer,
|
|
1960
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1961
|
-
scope: this.currentTurnScope,
|
|
1009
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1010
|
+
scope: this.engine.currentTurnScope,
|
|
1962
1011
|
runtimeSettings: this.runtimeSettings,
|
|
1012
|
+
spawnAt: this.spawnAt,
|
|
1963
1013
|
});
|
|
1964
|
-
return await Promise.resolve(this.
|
|
1014
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1965
1015
|
...input,
|
|
1966
1016
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1967
1017
|
}));
|
|
@@ -1973,7 +1023,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1973
1023
|
|
|
1974
1024
|
/** Whether this adapter has CLI scripts loaded */
|
|
1975
1025
|
hasCliScripts(): boolean {
|
|
1976
|
-
return
|
|
1026
|
+
return this.runner.hasDetectStatus();
|
|
1977
1027
|
}
|
|
1978
1028
|
|
|
1979
1029
|
/**
|
|
@@ -1982,24 +1032,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1982
1032
|
*/
|
|
1983
1033
|
async resolveAction(data: any): Promise<void> {
|
|
1984
1034
|
let promptText = '';
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
}
|
|
1035
|
+
try {
|
|
1036
|
+
promptText = this.runner.invokeByName('resolveAction', data);
|
|
1037
|
+
} catch {
|
|
1038
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
|
|
1039
|
+
return;
|
|
1991
1040
|
}
|
|
1992
1041
|
if (!promptText) {
|
|
1993
|
-
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not
|
|
1042
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
|
|
1994
1043
|
return;
|
|
1995
1044
|
}
|
|
1996
1045
|
await this.sendMessage(promptText);
|
|
1997
1046
|
}
|
|
1998
1047
|
|
|
1999
1048
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
2000
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
2001
|
-
if (this.hasActionableApproval()) return false;
|
|
2002
|
-
|
|
1049
|
+
if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
|
|
1050
|
+
if (this.engine.hasActionableApproval()) return false;
|
|
1051
|
+
// If there's already meaningful response content beyond the echoed prompt, not stuck
|
|
1052
|
+
if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
|
|
2003
1053
|
const screenText = this.terminalScreen.getText();
|
|
2004
1054
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
2005
1055
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -2008,20 +1058,41 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2008
1058
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
2009
1059
|
}
|
|
2010
1060
|
|
|
1061
|
+
private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
|
|
1062
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
1063
|
+
if (!raw) return false;
|
|
1064
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1065
|
+
if (!normalizedPrompt) return true;
|
|
1066
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
1067
|
+
if (!normalizedBuffer) return false;
|
|
1068
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1069
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1070
|
+
const remainder = normalizedBuffer
|
|
1071
|
+
.slice(normalizedPrompt.length)
|
|
1072
|
+
.replace(/[─═\-]+/g, '')
|
|
1073
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1074
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1075
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1076
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1077
|
+
.replace(/esctointerrupt/gi, '')
|
|
1078
|
+
.replace(/❯/g, '')
|
|
1079
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1080
|
+
.trim();
|
|
1081
|
+
return remainder.length > 0;
|
|
1082
|
+
}
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
2011
1086
|
private async writeToPty(data: string): Promise<void> {
|
|
2012
1087
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2013
1088
|
await this.ptyProcess.write(data);
|
|
2014
1089
|
}
|
|
2015
1090
|
|
|
2016
1091
|
private resetPendingSendState(reason: string): void {
|
|
2017
|
-
this.isWaitingForResponse = false;
|
|
2018
1092
|
this.responseBuffer = '';
|
|
2019
|
-
this.currentTurnScope = null;
|
|
2020
|
-
this.submitPendingUntil = 0;
|
|
2021
|
-
this.clearIdleFinishCandidate(reason);
|
|
2022
1093
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
2023
|
-
|
|
2024
|
-
|
|
1094
|
+
this.engine.resetActiveTurnState();
|
|
1095
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
2025
1096
|
}
|
|
2026
1097
|
|
|
2027
1098
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
@@ -2038,42 +1109,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2038
1109
|
}
|
|
2039
1110
|
this.responseTimeout = setTimeout(() => {
|
|
2040
1111
|
this.responseTimeout = null;
|
|
2041
|
-
if (!this.isWaitingForResponse) return;
|
|
2042
|
-
|
|
2043
|
-
const detectedStatusBeforeEval = this.runDetectStatus(this.recentOutputBuffer);
|
|
2044
|
-
this.recordTrace('response_timeout_check', {
|
|
2045
|
-
timeoutMs,
|
|
2046
|
-
detectedStatus: detectedStatusBeforeEval,
|
|
2047
|
-
currentStatus: this.currentStatus,
|
|
2048
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2049
|
-
hasActionableApproval: this.hasActionableApproval(),
|
|
2050
|
-
...buildCliTraceParseSnapshot({
|
|
2051
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
2052
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
2053
|
-
responseBuffer: this.responseBuffer,
|
|
2054
|
-
partialResponse: this.responseBuffer,
|
|
2055
|
-
scope: this.currentTurnScope,
|
|
2056
|
-
}),
|
|
2057
|
-
});
|
|
1112
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
2058
1113
|
|
|
2059
|
-
// maxResponse is a watchdog/checkpoint, not a completion signal.
|
|
2060
|
-
// behavior called finishResponse() unconditionally at the default 300s,
|
|
2061
|
-
// which fabricated idle transitions and downstream generating_completed
|
|
2062
|
-
// notifications while long-running CLIs were still generating. Re-run the
|
|
1114
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
2063
1115
|
// normal settled parser instead and keep the turn open unless the provider
|
|
2064
1116
|
// actually reports an idle, commit-ready state.
|
|
2065
|
-
this.
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
2069
|
-
const detectedStatusAfterEval = this.runDetectStatus(this.recentOutputBuffer);
|
|
2070
|
-
this.recordTrace('response_timeout_kept_open', {
|
|
2071
|
-
timeoutMs,
|
|
2072
|
-
detectedStatusBeforeEval,
|
|
2073
|
-
detectedStatusAfterEval,
|
|
2074
|
-
currentStatus: this.currentStatus,
|
|
2075
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2076
|
-
});
|
|
1117
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1118
|
+
|
|
1119
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
2077
1120
|
this.armResponseTimeout();
|
|
2078
1121
|
}
|
|
2079
1122
|
}, timeoutMs);
|
|
@@ -2088,34 +1131,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2088
1131
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
2089
1132
|
this.submitRetryTimer = null;
|
|
2090
1133
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
2091
|
-
|
|
2092
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1134
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2093
1135
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
|
|
2094
|
-
this.recordTrace('submit_write', {
|
|
2095
|
-
mode: 'submit_retry',
|
|
2096
|
-
attempt,
|
|
2097
|
-
sendKey: this.sendKey,
|
|
2098
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2099
|
-
});
|
|
2100
1136
|
this.writeSubmitKeyForRetry('submit_retry');
|
|
2101
|
-
if (attempt >= 3) { this.submitRetryUsed = true; return; }
|
|
1137
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
2102
1138
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
2103
1139
|
}
|
|
2104
1140
|
|
|
2105
1141
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
2106
1142
|
this.submitRetryTimer = null;
|
|
2107
1143
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
2108
|
-
|
|
2109
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1144
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2110
1145
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
|
|
2111
|
-
this.recordTrace('submit_write', {
|
|
2112
|
-
mode: 'immediate_retry',
|
|
2113
|
-
attempt: 1,
|
|
2114
|
-
sendKey: this.sendKey,
|
|
2115
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2116
|
-
});
|
|
2117
1146
|
this.writeSubmitKeyForRetry('immediate_retry');
|
|
2118
|
-
this.submitRetryUsed = true;
|
|
1147
|
+
this.engine.submitRetryUsed = true;
|
|
2119
1148
|
}
|
|
2120
1149
|
|
|
2121
1150
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -2123,13 +1152,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2123
1152
|
completion.resolveOnce();
|
|
2124
1153
|
return;
|
|
2125
1154
|
}
|
|
2126
|
-
this.submitPendingUntil = 0;
|
|
2127
|
-
const screenText = this.terminalScreen.getText();
|
|
2128
|
-
this.recordTrace('submit_write', {
|
|
2129
|
-
mode: 'submit_key',
|
|
2130
|
-
sendKey: this.sendKey,
|
|
2131
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
2132
|
-
});
|
|
1155
|
+
this.engine.submitPendingUntil = 0;
|
|
2133
1156
|
void this.writeToPty(this.sendKey).then(() => {
|
|
2134
1157
|
this.commitSendUserTurn(state);
|
|
2135
1158
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -2139,13 +1162,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2139
1162
|
}
|
|
2140
1163
|
|
|
2141
1164
|
private submitImmediatePrompt(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
2142
|
-
this.submitPendingUntil = 0;
|
|
2143
|
-
this.recordTrace('submit_write', {
|
|
2144
|
-
mode: 'immediate',
|
|
2145
|
-
text: summarizeCliTraceText(state.text, 500),
|
|
2146
|
-
sendKey: this.sendKey,
|
|
2147
|
-
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
|
|
2148
|
-
});
|
|
1165
|
+
this.engine.submitPendingUntil = 0;
|
|
2149
1166
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
2150
1167
|
this.commitSendUserTurn(state);
|
|
2151
1168
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -2189,7 +1206,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2189
1206
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2190
1207
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
2191
1208
|
};
|
|
2192
|
-
|
|
1209
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1210
|
+
|
|
2193
1211
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
2194
1212
|
// At this point the prompt text write already completed. Rejecting without
|
|
2195
1213
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -2226,13 +1244,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2226
1244
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2227
1245
|
const content = String(text || '');
|
|
2228
1246
|
if (!content.trim()) return;
|
|
2229
|
-
|
|
2230
|
-
text: summarizeCliTraceText(content, 500),
|
|
2231
|
-
status: this.currentStatus,
|
|
2232
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2233
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2234
|
-
});
|
|
2235
|
-
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.currentStatus}`);
|
|
1247
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
|
|
2236
1248
|
await this.writeToPty(content + this.sendKey);
|
|
2237
1249
|
this.onStatusChange?.();
|
|
2238
1250
|
}
|
|
@@ -2241,11 +1253,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2241
1253
|
const content = String(text || '');
|
|
2242
1254
|
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
2243
1255
|
if (duplicate) {
|
|
2244
|
-
this.recordTrace('send_message_queued_duplicate_suppressed', {
|
|
2245
|
-
reason,
|
|
2246
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2247
|
-
text: summarizeCliTraceText(content, 500),
|
|
2248
|
-
});
|
|
2249
1256
|
return;
|
|
2250
1257
|
}
|
|
2251
1258
|
const queuedAt = Date.now();
|
|
@@ -2257,24 +1264,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2257
1264
|
source: 'sendMessage',
|
|
2258
1265
|
};
|
|
2259
1266
|
this.pendingOutboundQueue.push(message);
|
|
2260
|
-
this.recordTrace('send_message_queued', {
|
|
2261
|
-
reason,
|
|
2262
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2263
|
-
queuedAt,
|
|
2264
|
-
text: summarizeCliTraceText(content, 500),
|
|
2265
|
-
});
|
|
2266
1267
|
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
2267
1268
|
this.onStatusChange?.();
|
|
2268
1269
|
}
|
|
2269
1270
|
|
|
2270
1271
|
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
2271
1272
|
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
2272
|
-
if (this.hasActionableApproval()) return null;
|
|
1273
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
2273
1274
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
2274
1275
|
? String(parsedStatusBeforeSend.status)
|
|
2275
1276
|
: '';
|
|
2276
|
-
|
|
2277
|
-
|
|
1277
|
+
const hasFinalAssistant = (p: any) => {
|
|
1278
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1279
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1280
|
+
};
|
|
1281
|
+
if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
|
|
1282
|
+
if (this.engine.currentStatus === 'generating') return 'current_status_generating';
|
|
2278
1283
|
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
|
|
2279
1284
|
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
2280
1285
|
const parsedHasActionableModal = Boolean(
|
|
@@ -2282,15 +1287,15 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2282
1287
|
&& Array.isArray(parsedModal.buttons)
|
|
2283
1288
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
2284
1289
|
);
|
|
2285
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1290
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
2286
1291
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
2287
|
-
&& !this.isWaitingForResponse
|
|
2288
|
-
&& !this.currentTurnScope
|
|
2289
|
-
&& !this.hasActionableApproval()
|
|
1292
|
+
&& !this.engine.isWaitingForResponse
|
|
1293
|
+
&& !this.engine.currentTurnScope
|
|
1294
|
+
&& !this.engine.hasActionableApproval()
|
|
2290
1295
|
&& !parsedHasActionableModal;
|
|
2291
1296
|
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
2292
1297
|
}
|
|
2293
|
-
if (this.isWaitingForResponse && this.currentTurnScope) return 'active_turn_in_progress';
|
|
1298
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
2294
1299
|
return null;
|
|
2295
1300
|
}
|
|
2296
1301
|
|
|
@@ -2304,18 +1309,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2304
1309
|
|
|
2305
1310
|
private async flushPendingOutboundQueue(): Promise<void> {
|
|
2306
1311
|
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
2307
|
-
if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) return;
|
|
1312
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
2308
1313
|
this.pendingOutboundFlushInFlight = true;
|
|
2309
1314
|
try {
|
|
2310
1315
|
while (this.pendingOutboundQueue.length > 0) {
|
|
2311
|
-
if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) break;
|
|
1316
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
2312
1317
|
const next = this.pendingOutboundQueue[0];
|
|
2313
|
-
this.recordTrace('send_message_queue_flush', {
|
|
2314
|
-
id: next.id,
|
|
2315
|
-
queuedAt: next.queuedAt,
|
|
2316
|
-
queueLength: this.pendingOutboundQueue.length,
|
|
2317
|
-
text: summarizeCliTraceText(next.content, 500),
|
|
2318
|
-
});
|
|
2319
1318
|
try {
|
|
2320
1319
|
await this.sendMessageNow(next.content, false);
|
|
2321
1320
|
this.pendingOutboundQueue.shift();
|
|
@@ -2335,8 +1334,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2335
1334
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2336
1335
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
2337
1336
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
2338
|
-
&& this.isWaitingForResponse
|
|
2339
|
-
&& !this.hasActionableApproval();
|
|
1337
|
+
&& this.engine.isWaitingForResponse
|
|
1338
|
+
&& !this.engine.hasActionableApproval();
|
|
2340
1339
|
if (this.startupParseGate) {
|
|
2341
1340
|
const deadline = Date.now() + 10000;
|
|
2342
1341
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -2366,11 +1365,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2366
1365
|
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
2367
1366
|
this.ready = true;
|
|
2368
1367
|
this.startupParseGate = false;
|
|
2369
|
-
this.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1368
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
2370
1369
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
2371
1370
|
}
|
|
2372
1371
|
}
|
|
2373
|
-
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
|
|
1372
|
+
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
2374
1373
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
2375
1374
|
? String(parsedStatusBeforeSend.status)
|
|
2376
1375
|
: '';
|
|
@@ -2381,11 +1380,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2381
1380
|
&& Array.isArray(parsedModal.buttons)
|
|
2382
1381
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
2383
1382
|
);
|
|
2384
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1383
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
2385
1384
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
2386
|
-
&& !this.isWaitingForResponse
|
|
2387
|
-
&& !this.currentTurnScope
|
|
2388
|
-
&& !this.hasActionableApproval()
|
|
1385
|
+
&& !this.engine.isWaitingForResponse
|
|
1386
|
+
&& !this.engine.currentTurnScope
|
|
1387
|
+
&& !this.engine.hasActionableApproval()
|
|
2389
1388
|
&& !parsedHasActionableModal;
|
|
2390
1389
|
if (!terminalLooksIdle) {
|
|
2391
1390
|
if (allowQueue) {
|
|
@@ -2395,10 +1394,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2395
1394
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
2396
1395
|
}
|
|
2397
1396
|
}
|
|
2398
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1397
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1398
|
+
const snap = this.getSnapshot();
|
|
2399
1399
|
if (
|
|
2400
|
-
!this.clearStaleIdleResponseGuard('send_message_guard')
|
|
2401
|
-
&& !this.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend)
|
|
1400
|
+
!this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
|
|
1401
|
+
&& !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
|
|
2402
1402
|
) {
|
|
2403
1403
|
if (allowQueue) {
|
|
2404
1404
|
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
@@ -2407,30 +1407,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2407
1407
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
2408
1408
|
}
|
|
2409
1409
|
}
|
|
2410
|
-
this.isWaitingForResponse = true;
|
|
2411
1410
|
this.responseBuffer = '';
|
|
2412
|
-
|
|
2413
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
2414
|
-
this.clearIdleFinishCandidate('send_message');
|
|
2415
|
-
this.currentTurnScope = {
|
|
1411
|
+
const turnScope: TurnParseScope = {
|
|
2416
1412
|
prompt: text,
|
|
2417
1413
|
startedAt: Date.now(),
|
|
2418
1414
|
bufferStart: this.accumulatedBuffer.length,
|
|
2419
1415
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
2420
1416
|
};
|
|
2421
|
-
|
|
2422
|
-
text: summarizeCliTraceText(text, 500),
|
|
2423
|
-
estimatedLines: estimatePromptDisplayLines(text),
|
|
2424
|
-
turnScope: this.currentTurnScope,
|
|
2425
|
-
});
|
|
2426
|
-
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2427
|
-
this.submitRetryUsed = false;
|
|
2428
|
-
this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
2429
|
-
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
|
|
1417
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2430
1418
|
if (this.submitRetryTimer) {
|
|
2431
1419
|
clearTimeout(this.submitRetryTimer);
|
|
2432
1420
|
this.submitRetryTimer = null;
|
|
2433
1421
|
}
|
|
1422
|
+
this.engine.onTurnStarted(turnScope);
|
|
1423
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1424
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2434
1425
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2435
1426
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2436
1427
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2443,12 +1434,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2443
1434
|
retryDelayMs,
|
|
2444
1435
|
didCommitUserTurn: false,
|
|
2445
1436
|
};
|
|
2446
|
-
|
|
2447
|
-
clearTimeout(this.settleTimer);
|
|
2448
|
-
this.settleTimer = null;
|
|
2449
|
-
}
|
|
2450
|
-
this.responseEpoch += 1;
|
|
2451
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1437
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2452
1438
|
await new Promise<void>((resolve, reject) => {
|
|
2453
1439
|
let resolved = false;
|
|
2454
1440
|
const completion: SendMessageCompletion = {
|
|
@@ -2471,24 +1457,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2471
1457
|
}
|
|
2472
1458
|
|
|
2473
1459
|
if (submitDelayMs > 0) {
|
|
2474
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1460
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2475
1461
|
}
|
|
2476
|
-
this.recordTrace('submit_write', {
|
|
2477
|
-
mode: 'type_then_submit',
|
|
2478
|
-
text: summarizeCliTraceText(text, 500),
|
|
2479
|
-
sendKey: this.sendKey,
|
|
2480
|
-
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
|
|
2481
|
-
});
|
|
2482
1462
|
const submitStartedAt = Date.now();
|
|
2483
1463
|
void this.writeToPty(text).then(
|
|
2484
1464
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2485
1465
|
completion.rejectOnce,
|
|
2486
1466
|
);
|
|
2487
1467
|
});
|
|
1468
|
+
// Schedule settle after successful send
|
|
1469
|
+
this.engine.scheduleSettle();
|
|
2488
1470
|
}
|
|
2489
1471
|
|
|
2490
1472
|
getPartialResponse(): string {
|
|
2491
|
-
if (!this.isWaitingForResponse) return '';
|
|
1473
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2492
1474
|
return this.responseBuffer;
|
|
2493
1475
|
}
|
|
2494
1476
|
|
|
@@ -2501,10 +1483,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2501
1483
|
cliType: this.cliType,
|
|
2502
1484
|
cliName: this.cliName,
|
|
2503
1485
|
workingDir: this.workingDir,
|
|
2504
|
-
currentStatus: this.currentStatus,
|
|
1486
|
+
currentStatus: this.engine.currentStatus,
|
|
2505
1487
|
ready: this.ready,
|
|
2506
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2507
|
-
activeModal: this.activeModal,
|
|
1488
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1489
|
+
activeModal: this.engine.activeModal,
|
|
2508
1490
|
parseErrorMessage: this.parseErrorMessage,
|
|
2509
1491
|
messageCounts: {
|
|
2510
1492
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2530,10 +1512,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2530
1512
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2531
1513
|
},
|
|
2532
1514
|
parser: {
|
|
2533
|
-
scriptNames:
|
|
2534
|
-
traceSessionId: this.
|
|
2535
|
-
traceSeq: this.
|
|
2536
|
-
currentTurnScope: this.currentTurnScope,
|
|
1515
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1516
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1517
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1518
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2537
1519
|
parsedStatusCache: parsedResult
|
|
2538
1520
|
? {
|
|
2539
1521
|
id: parsedResult.id,
|
|
@@ -2546,26 +1528,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2546
1528
|
activeModal: parsedResult.activeModal,
|
|
2547
1529
|
}
|
|
2548
1530
|
: null,
|
|
2549
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2550
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1531
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1532
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2551
1533
|
},
|
|
2552
1534
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2553
|
-
statusHistory: this.
|
|
2554
|
-
traceEntries: this.
|
|
1535
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1536
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2555
1537
|
timing: {
|
|
2556
1538
|
spawnAt: this.spawnAt,
|
|
2557
1539
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2558
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2559
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2560
|
-
responseEpoch: this.responseEpoch,
|
|
1540
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1541
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1542
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2561
1543
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2562
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1544
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2563
1545
|
},
|
|
2564
1546
|
finish: {
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1547
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1548
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1549
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2569
1550
|
},
|
|
2570
1551
|
};
|
|
2571
1552
|
}
|
|
@@ -2602,7 +1583,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2602
1583
|
this.timeouts.shutdownGrace,
|
|
2603
1584
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2604
1585
|
);
|
|
2605
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1586
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2606
1587
|
|
|
2607
1588
|
try {
|
|
2608
1589
|
if (wasProcessing) {
|
|
@@ -2640,7 +1621,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2640
1621
|
return new Promise((resolve) => {
|
|
2641
1622
|
const startedAt = Date.now();
|
|
2642
1623
|
const timer = setInterval(() => {
|
|
2643
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
1624
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2644
1625
|
clearInterval(timer);
|
|
2645
1626
|
resolve(true);
|
|
2646
1627
|
return;
|
|
@@ -2654,12 +1635,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2654
1635
|
}
|
|
2655
1636
|
|
|
2656
1637
|
shutdown(): void {
|
|
2657
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
1638
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2658
1639
|
this.clearAllTimers();
|
|
2659
1640
|
this.pendingOutputParseChunks = [];
|
|
2660
1641
|
this.pendingTerminalQueryTail = '';
|
|
2661
1642
|
this.ptyOutputChunks = [];
|
|
2662
|
-
this.finishRetryCount = 0;
|
|
2663
1643
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2664
1644
|
this.pendingOutboundQueue = [];
|
|
2665
1645
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2668,7 +1648,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2668
1648
|
setTimeout(() => {
|
|
2669
1649
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2670
1650
|
this.ptyProcess = null;
|
|
2671
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
1651
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2672
1652
|
this.ready = false;
|
|
2673
1653
|
this.startupParseGate = false;
|
|
2674
1654
|
this.spawnAt = 0;
|
|
@@ -2678,12 +1658,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2678
1658
|
}
|
|
2679
1659
|
|
|
2680
1660
|
detach(): void {
|
|
2681
|
-
this.clearIdleFinishCandidate('detach');
|
|
1661
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2682
1662
|
this.clearAllTimers();
|
|
2683
1663
|
this.pendingOutputParseChunks = [];
|
|
2684
1664
|
this.pendingTerminalQueryTail = '';
|
|
2685
1665
|
this.ptyOutputChunks = [];
|
|
2686
|
-
this.finishRetryCount = 0;
|
|
2687
1666
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2688
1667
|
this.pendingOutboundQueue = [];
|
|
2689
1668
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2704,19 +1683,17 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2704
1683
|
}
|
|
2705
1684
|
|
|
2706
1685
|
clearHistory(): void {
|
|
2707
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
1686
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2708
1687
|
this.accumulatedBuffer = '';
|
|
2709
1688
|
this.accumulatedRawBuffer = '';
|
|
2710
|
-
this.currentTurnScope = null;
|
|
2711
|
-
this.submitRetryUsed = false;
|
|
2712
|
-
this.submitRetryPromptSnippet = '';
|
|
1689
|
+
this.engine.currentTurnScope = null;
|
|
1690
|
+
this.engine.submitRetryUsed = false;
|
|
1691
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2713
1692
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2714
1693
|
this.pendingOutputParseChunks = [];
|
|
2715
1694
|
this.pendingTerminalQueryTail = '';
|
|
2716
1695
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2717
1696
|
this.ptyOutputChunks = [];
|
|
2718
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
2719
|
-
this.finishRetryCount = 0;
|
|
2720
1697
|
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2721
1698
|
this.pendingOutboundQueue = [];
|
|
2722
1699
|
this.pendingOutboundFlushInFlight = false;
|
|
@@ -2725,76 +1702,109 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2725
1702
|
this.onStatusChange?.();
|
|
2726
1703
|
}
|
|
2727
1704
|
|
|
2728
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
1705
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2729
1706
|
isReady(): boolean { return this.ready; }
|
|
2730
1707
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
1708
|
+
// ─── State machine property accessors (delegate to engine) ──────────────
|
|
1709
|
+
// These expose engine state for external callers (tests, debug tools, etc.)
|
|
1710
|
+
|
|
1711
|
+
get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
|
|
1712
|
+
set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
|
|
1713
|
+
|
|
1714
|
+
get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
|
|
1715
|
+
set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
|
|
1716
|
+
|
|
1717
|
+
get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
|
|
1718
|
+
set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
|
|
1719
|
+
|
|
1720
|
+
get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
|
|
1721
|
+
set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
|
|
1722
|
+
|
|
1723
|
+
get responseEpoch(): number { return this.engine.responseEpoch; }
|
|
1724
|
+
set responseEpoch(v: number) { this.engine.responseEpoch = v; }
|
|
1725
|
+
|
|
1726
|
+
get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
|
|
1727
|
+
set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
|
|
1728
|
+
|
|
1729
|
+
get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
|
|
1730
|
+
set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
|
|
1731
|
+
|
|
1732
|
+
get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
|
|
1733
|
+
set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
|
|
1734
|
+
|
|
1735
|
+
get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
|
|
1736
|
+
set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
|
|
1737
|
+
|
|
1738
|
+
get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
|
|
1739
|
+
set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
|
|
1740
|
+
|
|
1741
|
+
get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
|
|
1742
|
+
get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
|
|
1743
|
+
|
|
1744
|
+
get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
|
|
1745
|
+
get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
|
|
1746
|
+
|
|
1747
|
+
get finishRetryCount(): number { return this.engine.finishRetryCount; }
|
|
1748
|
+
set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
|
|
1749
|
+
|
|
1750
|
+
get traceSessionId(): string { return this.engine.getTraceSessionId(); }
|
|
1751
|
+
get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
|
|
1752
|
+
get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
|
|
1753
|
+
get traceSeq(): number { return this.engine.getTraceEntries().length; }
|
|
1754
|
+
|
|
1755
|
+
/** Expose engine's evaluateSettled for test access */
|
|
1756
|
+
evaluateSettled(): void {
|
|
1757
|
+
LOG.debug(
|
|
1758
|
+
'CLI',
|
|
1759
|
+
`[${this.cliType}] settled diagnostics delegated to state engine`);
|
|
1760
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1761
|
+
}
|
|
1762
|
+
/** Expose engine's scheduleSettle for test access */
|
|
1763
|
+
scheduleSettle(): void { this.engine.scheduleSettle(); }
|
|
1764
|
+
/** Expose engine's clearIdleFinishCandidate for test access */
|
|
1765
|
+
clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
|
|
1766
|
+
/** Expose engine's finishResponse for test access */
|
|
1767
|
+
finishResponse(): void { this.engine.finishResponse(); }
|
|
1768
|
+
/** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
|
|
1769
|
+
getSnapshot(): CliBufferSnapshot {
|
|
1770
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
1771
|
+
return {
|
|
1772
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
1773
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1774
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
1775
|
+
responseBuffer: this.responseBuffer,
|
|
1776
|
+
screenText,
|
|
1777
|
+
parseScreenText: this.getParseScreenText(screenText),
|
|
1778
|
+
workingDir: this.workingDir,
|
|
1779
|
+
providerSessionId: this.providerSessionId,
|
|
1780
|
+
runtimeSettings: this.runtimeSettings,
|
|
1781
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1782
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1783
|
+
lastOutputAt: this.lastOutputAt,
|
|
1784
|
+
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
1785
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1786
|
+
lastScreenSnapshot: this.lastScreenSnapshot,
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
isAlive(): boolean { return this.ptyProcess !== null; }
|
|
1790
|
+
flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
|
|
1791
|
+
|
|
1792
|
+
async writeRaw(data: string | Buffer): Promise<void> {
|
|
1793
|
+
const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
|
1794
|
+
await this.writeToPty(str);
|
|
2737
1795
|
}
|
|
2738
1796
|
|
|
2739
1797
|
resolveModal(buttonIndex: number): void {
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
return;
|
|
2746
|
-
}
|
|
2747
|
-
let modal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
|
|
2748
|
-
if (!modal && typeof this.cliScripts?.parseSession === 'function') {
|
|
2749
|
-
try {
|
|
2750
|
-
const parsed = this.getScriptParsedStatus();
|
|
2751
|
-
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
2752
|
-
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
2753
|
-
? parsed.activeModal
|
|
2754
|
-
: null;
|
|
2755
|
-
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
2756
|
-
modal = parsedModal;
|
|
2757
|
-
this.activeModal = parsedModal;
|
|
2758
|
-
if (this.currentStatus !== 'waiting_approval') {
|
|
2759
|
-
this.setStatus('waiting_approval', 'resolve_modal_parse');
|
|
2760
|
-
this.onStatusChange?.();
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
} catch {
|
|
2764
|
-
// Ignore parse failures here; resolveModal falls back to current state.
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
if (!this.ptyProcess || ((this.currentStatus !== 'waiting_approval') && !modal)) return;
|
|
2768
|
-
this.clearIdleFinishCandidate('resolve_modal');
|
|
2769
|
-
this.recordTrace('resolve_modal', {
|
|
2770
|
-
buttonIndex,
|
|
2771
|
-
activeModal: modal,
|
|
2772
|
-
});
|
|
2773
|
-
this.activeModal = null;
|
|
2774
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
2775
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2776
|
-
if (this.approvalExitTimeout) {
|
|
2777
|
-
clearTimeout(this.approvalExitTimeout);
|
|
2778
|
-
this.approvalExitTimeout = null;
|
|
2779
|
-
}
|
|
2780
|
-
this.setStatus('generating', 'approval_resolved');
|
|
2781
|
-
this.onStatusChange?.();
|
|
2782
|
-
if (buttonIndex in this.approvalKeys) {
|
|
2783
|
-
this.ptyProcess.write(this.approvalKeys[buttonIndex]);
|
|
2784
|
-
} else {
|
|
2785
|
-
const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
|
|
2786
|
-
const clampedIndex = buttonCount > 0
|
|
2787
|
-
? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
|
|
2788
|
-
: Math.max(0, buttonIndex);
|
|
2789
|
-
const DOWN = '\x1B[B';
|
|
2790
|
-
const keys = DOWN.repeat(clampedIndex) + '\r';
|
|
2791
|
-
this.ptyProcess.write(keys);
|
|
2792
|
-
}
|
|
1798
|
+
this.engine.resolveModal(buttonIndex);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
1802
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
2793
1803
|
}
|
|
2794
1804
|
|
|
2795
1805
|
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
2796
1806
|
isApprovalRecentlyResolved(): boolean {
|
|
2797
|
-
return
|
|
1807
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2798
1808
|
}
|
|
2799
1809
|
|
|
2800
1810
|
resize(cols: number, rows: number): void {
|
|
@@ -2808,7 +1818,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2808
1818
|
}
|
|
2809
1819
|
|
|
2810
1820
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2811
|
-
if (this.startupParseGate ||
|
|
1821
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2812
1822
|
try {
|
|
2813
1823
|
const parsed = this.getScriptParsedStatus();
|
|
2814
1824
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2826,6 +1836,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2826
1836
|
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2827
1837
|
const parsedDebugState = this.getParsedDebugState();
|
|
2828
1838
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
1839
|
+
const hasFinalAssistant = (p: any) => {
|
|
1840
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1841
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1842
|
+
};
|
|
2829
1843
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
2830
1844
|
if (parsedDebugState?.status === 'error') {
|
|
2831
1845
|
effectiveStatus = 'error';
|
|
@@ -2836,7 +1850,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2836
1850
|
if (
|
|
2837
1851
|
effectiveStatus === 'idle'
|
|
2838
1852
|
&& parsedDebugState?.status === 'generating'
|
|
2839
|
-
&& !
|
|
1853
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
2840
1854
|
) {
|
|
2841
1855
|
effectiveStatus = 'generating';
|
|
2842
1856
|
}
|
|
@@ -2846,8 +1860,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2846
1860
|
providerResolution: this.providerResolutionMeta,
|
|
2847
1861
|
status: effectiveStatus,
|
|
2848
1862
|
projectedStatus: effectiveStatus,
|
|
2849
|
-
rawStatus: this.currentStatus,
|
|
2850
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1863
|
+
rawStatus: this.engine.currentStatus,
|
|
1864
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2851
1865
|
ready: effectiveReady,
|
|
2852
1866
|
startupParseGate: this.startupParseGate,
|
|
2853
1867
|
spawnAt: this.spawnAt,
|
|
@@ -2867,10 +1881,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2867
1881
|
messageCount: parsedMessages.length,
|
|
2868
1882
|
} : null,
|
|
2869
1883
|
screenText: screenText.slice(-4000),
|
|
2870
|
-
currentTurnScope: this.currentTurnScope,
|
|
1884
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2871
1885
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2872
1886
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2873
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2874
1887
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2875
1888
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2876
1889
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
@@ -2888,21 +1901,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2888
1901
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2889
1902
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2890
1903
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2891
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2892
|
-
activeModal: startupModal || this.activeModal,
|
|
2893
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1904
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1905
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
1906
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2894
1907
|
sendDelayMs: this.sendDelayMs,
|
|
2895
1908
|
sendKey: this.sendKey,
|
|
2896
1909
|
submitStrategy: this.submitStrategy,
|
|
2897
1910
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2898
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2899
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1911
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1912
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2900
1913
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2901
1914
|
hasCliScripts: this.hasCliScripts(),
|
|
2902
|
-
scriptNames:
|
|
2903
|
-
traceSessionId: this.
|
|
2904
|
-
traceEntryCount: this.
|
|
2905
|
-
statusHistory: this.
|
|
1915
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1916
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1917
|
+
traceEntryCount: this.engine.getTraceEntries().length,
|
|
1918
|
+
statusHistory: this.engine.getStatusHistory().slice(-30),
|
|
2906
1919
|
timeouts: this.timeouts,
|
|
2907
1920
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2908
1921
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2912,20 +1925,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2912
1925
|
|
|
2913
1926
|
getTraceState(limit = 120): Record<string, any> {
|
|
2914
1927
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
1928
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2915
1929
|
return {
|
|
2916
|
-
sessionId: this.
|
|
1930
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2917
1931
|
providerResolution: this.providerResolutionMeta,
|
|
2918
|
-
entryCount:
|
|
2919
|
-
entries:
|
|
1932
|
+
entryCount: traceEntries.length,
|
|
1933
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2920
1934
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2921
1935
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2922
1936
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2923
1937
|
status: this.projectEffectiveStatus(),
|
|
2924
1938
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2925
|
-
rawStatus: this.currentStatus,
|
|
2926
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2927
|
-
activeModal: this.activeModal,
|
|
2928
|
-
currentTurnScope: this.currentTurnScope,
|
|
1939
|
+
rawStatus: this.engine.currentStatus,
|
|
1940
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1941
|
+
activeModal: this.engine.activeModal,
|
|
1942
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2929
1943
|
messages: [],
|
|
2930
1944
|
};
|
|
2931
1945
|
}
|