@adhdev/daemon-core 0.9.82-rc.14 → 0.9.82-rc.141
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/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +5 -1
- package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +87 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +21 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +5 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +68 -1
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +18 -6
- package/dist/index.js +10431 -2827
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10376 -2811
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +72 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-registry.d.ts +25 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +77 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +44 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
- package/dist/providers/approval-utils.d.ts +9 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +6 -1
- 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 +67 -0
- package/dist/shared-types.d.ts +12 -0
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +3 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +14 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +3 -1
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +1083 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +630 -1137
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +51 -11
- 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 +1428 -50
- package/src/commands/cli-manager.ts +145 -3
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3271 -427
- package/src/config/chat-history.ts +79 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +249 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +17 -5
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +45 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +479 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +40 -22
- package/src/mesh/coordinator-registry.ts +75 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +799 -56
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +216 -158
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- package/src/providers/approval-utils.ts +39 -5
- package/src/providers/chat-message-normalization.ts +7 -12
- package/src/providers/cli-provider-instance.ts +362 -41
- package/src/providers/contracts.ts +19 -0
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +31 -11
- package/src/providers/provider-schema.ts +12 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +77 -0
- package/src/shared-types.ts +9 -0
- package/src/status/builders.ts +23 -6
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +35 -11
- package/src/system/host-memory.ts +29 -12
- 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;
|
|
@@ -110,6 +99,14 @@ interface SendMessageCompletion {
|
|
|
110
99
|
rejectOnce: (error: unknown) => void;
|
|
111
100
|
}
|
|
112
101
|
|
|
102
|
+
interface PendingOutboundMessage {
|
|
103
|
+
id: string;
|
|
104
|
+
role: 'user';
|
|
105
|
+
content: string;
|
|
106
|
+
queuedAt: number;
|
|
107
|
+
source: 'sendMessage';
|
|
108
|
+
}
|
|
109
|
+
|
|
113
110
|
export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
|
|
114
111
|
if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
|
|
115
112
|
if (maxChars <= 0) return '';
|
|
@@ -122,23 +119,23 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
|
|
|
122
119
|
// ─── Adapter ────────────────────────────────────────
|
|
123
120
|
|
|
124
121
|
export class ProviderCliAdapter implements CliAdapter {
|
|
125
|
-
|
|
122
|
+
cliType: string;
|
|
126
123
|
readonly cliName: string;
|
|
127
124
|
public workingDir: string;
|
|
128
125
|
|
|
129
126
|
private provider: CliProviderModule;
|
|
130
127
|
private ptyProcess: PtyRuntimeTransport | null = null;
|
|
131
128
|
private transportFactory: PtyTransportFactory;
|
|
132
|
-
private currentStatus: CliSessionStatus['status'] = 'starting';
|
|
133
129
|
private onStatusChange: (() => void) | null = null;
|
|
134
130
|
|
|
131
|
+
// ─── State machine engine ─────────────────────────
|
|
132
|
+
readonly engine: CliStateEngine;
|
|
133
|
+
|
|
135
134
|
private responseBuffer = '';
|
|
136
135
|
private recentOutputBuffer = '';
|
|
137
|
-
private
|
|
138
|
-
private
|
|
139
|
-
private parseErrorMessage: string | null = null;
|
|
136
|
+
private get parseErrorMessage(): string | null { return this.runner.parseErrorMessage; }
|
|
137
|
+
private providerSessionId: string | null = null;
|
|
140
138
|
private responseTimeout: NodeJS.Timeout | null = null;
|
|
141
|
-
private idleTimeout: NodeJS.Timeout | null = null;
|
|
142
139
|
private ready = false;
|
|
143
140
|
private startupBuffer = '';
|
|
144
141
|
private startupParseGate = false;
|
|
@@ -164,39 +161,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
164
161
|
private serverConn: any = null;
|
|
165
162
|
private logBuffer: { message: string; level: string }[] = [];
|
|
166
163
|
|
|
167
|
-
|
|
168
|
-
private
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
private approvalTransitionBuffer: string = '';
|
|
172
|
-
private approvalExitTimeout: NodeJS.Timeout | null = null;
|
|
173
|
-
private pendingScriptStatus: 'generating' | 'waiting_approval' | null = null;
|
|
174
|
-
private pendingScriptStatusSince = 0;
|
|
175
|
-
private pendingScriptStatusTimer: NodeJS.Timeout | null = null;
|
|
176
|
-
|
|
177
|
-
// Output settle debounce — fires after PTY output goes quiet
|
|
178
|
-
private settleTimer: NodeJS.Timeout | null = null;
|
|
179
|
-
private settledBuffer: string = '';
|
|
180
|
-
private submitPendingUntil = 0;
|
|
181
|
-
private responseSettleIgnoreUntil = 0;
|
|
182
|
-
private responseEpoch = 0;
|
|
164
|
+
private pendingOutboundQueue: PendingOutboundMessage[] = [];
|
|
165
|
+
private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
|
|
166
|
+
private pendingOutboundFlushInFlight = false;
|
|
167
|
+
// Submit retry timer — PTY-level, not state machine
|
|
183
168
|
private submitRetryTimer: NodeJS.Timeout | null = null;
|
|
184
|
-
private submitRetryUsed = false;
|
|
185
|
-
private submitRetryPromptSnippet = '';
|
|
186
|
-
private idleFinishCandidate: IdleFinishCandidate | null = null;
|
|
187
|
-
private finishRetryTimer: NodeJS.Timeout | null = null;
|
|
188
|
-
private finishRetryCount = 0;
|
|
189
169
|
|
|
190
170
|
// Resize redraw suppression
|
|
191
171
|
private resizeSuppressUntil: number = 0;
|
|
192
172
|
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
195
177
|
|
|
196
|
-
// ───
|
|
197
|
-
private
|
|
198
|
-
/**
|
|
199
|
-
|
|
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); }
|
|
200
183
|
private runtimeSettings: Record<string, any> = {};
|
|
201
184
|
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
202
185
|
private accumulatedBuffer: string = '';
|
|
@@ -216,10 +199,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
216
199
|
* Hermes turn (tool calls + reasoning + final bubble) without the
|
|
217
200
|
* rolling window pushing the turn's ╭─ opening line out of view. */
|
|
218
201
|
private static readonly MAX_ACCUMULATED_BUFFER = 262144;
|
|
219
|
-
private currentTurnScope: TurnParseScope | null = null;
|
|
220
|
-
private traceEntries: CliTraceEntry[] = [];
|
|
221
|
-
private traceSeq = 0;
|
|
222
|
-
private traceSessionId = '';
|
|
223
202
|
private parsedStatusCache: {
|
|
224
203
|
responseBuffer: string;
|
|
225
204
|
currentTurnScope: TurnParseScope | null;
|
|
@@ -233,11 +212,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
233
212
|
result: any;
|
|
234
213
|
} | null = null;
|
|
235
214
|
private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
236
|
-
private static readonly MAX_TRACE_ENTRIES = 250;
|
|
237
215
|
|
|
238
216
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
239
|
-
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
240
|
-
private static readonly MAX_FINISH_RETRIES = 2;
|
|
241
217
|
|
|
242
218
|
private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
|
|
243
219
|
const build = (droppedChars: number, maxChars: number) => droppedChars > 0
|
|
@@ -313,13 +289,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
313
289
|
if (
|
|
314
290
|
cached
|
|
315
291
|
&& cached.responseBuffer === this.responseBuffer
|
|
316
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
292
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
317
293
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
318
294
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
319
295
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
320
296
|
&& cached.screenText === this.lastScreenText
|
|
321
|
-
&& cached.currentStatus === this.currentStatus
|
|
322
|
-
&& cached.activeModal === this.activeModal
|
|
297
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
298
|
+
&& cached.activeModal === this.engine.activeModal
|
|
323
299
|
&& cached.cliName === this.cliName
|
|
324
300
|
) {
|
|
325
301
|
return cached.result;
|
|
@@ -343,86 +319,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
343
319
|
return this.timeouts.statusActivityHold;
|
|
344
320
|
}
|
|
345
321
|
|
|
346
|
-
private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
|
|
347
|
-
const prev = this.currentStatus;
|
|
348
|
-
if (prev === status) return;
|
|
349
|
-
this.currentStatus = status;
|
|
350
|
-
this.statusHistory.push({ status, at: Date.now(), trigger });
|
|
351
|
-
if (this.statusHistory.length > 50) this.statusHistory.shift();
|
|
352
|
-
this.recordTrace('status', {
|
|
353
|
-
previousStatus: prev,
|
|
354
|
-
trigger: trigger || null,
|
|
355
|
-
});
|
|
356
|
-
LOG.info('CLI', `[${this.cliType}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
private clearIdleFinishCandidate(reason: string): void {
|
|
360
|
-
if (!this.idleFinishCandidate) return;
|
|
361
|
-
this.recordTrace('idle_candidate_reset', {
|
|
362
|
-
reason,
|
|
363
|
-
candidate: this.idleFinishCandidate,
|
|
364
|
-
});
|
|
365
|
-
this.idleFinishCandidate = null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
private armIdleFinishCandidate(assistantLength: number): void {
|
|
369
|
-
const now = Date.now();
|
|
370
|
-
const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
|
|
371
|
-
this.idleFinishCandidate = {
|
|
372
|
-
armedAt: now,
|
|
373
|
-
lastOutputAt: this.lastOutputAt,
|
|
374
|
-
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
375
|
-
responseEpoch: this.responseEpoch,
|
|
376
|
-
assistantLength,
|
|
377
|
-
};
|
|
378
|
-
this.recordTrace('idle_candidate_armed', {
|
|
379
|
-
confirmMs: idleFinishConfirmMs,
|
|
380
|
-
candidate: this.idleFinishCandidate,
|
|
381
|
-
...buildCliTraceParseSnapshot({
|
|
382
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
383
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
384
|
-
responseBuffer: this.responseBuffer,
|
|
385
|
-
partialResponse: this.responseBuffer,
|
|
386
|
-
scope: this.currentTurnScope,
|
|
387
|
-
}),
|
|
388
|
-
});
|
|
389
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
390
|
-
this.settleTimer = setTimeout(() => {
|
|
391
|
-
this.settleTimer = null;
|
|
392
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
393
|
-
this.evaluateSettled();
|
|
394
|
-
}, idleFinishConfirmMs);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
private recordTrace(type: string, payload: Record<string, any> = {}): void {
|
|
399
|
-
const entry: CliTraceEntry = {
|
|
400
|
-
id: ++this.traceSeq,
|
|
401
|
-
at: Date.now(),
|
|
402
|
-
type,
|
|
403
|
-
status: this.currentStatus,
|
|
404
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
405
|
-
activeModal: this.activeModal
|
|
406
|
-
? { message: this.activeModal.message, buttons: [...this.activeModal.buttons] }
|
|
407
|
-
: null,
|
|
408
|
-
payload,
|
|
409
|
-
};
|
|
410
|
-
this.traceEntries.push(entry);
|
|
411
|
-
if (this.traceEntries.length > ProviderCliAdapter.MAX_TRACE_ENTRIES) {
|
|
412
|
-
this.traceEntries.splice(0, this.traceEntries.length - ProviderCliAdapter.MAX_TRACE_ENTRIES);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
private resetTraceSession(): void {
|
|
417
|
-
this.traceEntries = [];
|
|
418
|
-
this.traceSeq = 0;
|
|
419
|
-
this.traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
420
|
-
this.recordTrace('session_start', {
|
|
421
|
-
providerType: this.cliType,
|
|
422
|
-
workingDir: this.workingDir,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
322
|
// Resolved timeouts
|
|
427
323
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
428
324
|
|
|
@@ -432,7 +328,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
432
328
|
private readonly sendKey: string;
|
|
433
329
|
private readonly submitStrategy: 'wait_for_echo' | 'immediate';
|
|
434
330
|
private readonly requirePromptEchoBeforeSubmit: boolean;
|
|
435
|
-
private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
|
|
436
331
|
|
|
437
332
|
constructor(
|
|
438
333
|
provider: CliProviderModule,
|
|
@@ -441,6 +336,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
441
336
|
private extraEnv: Record<string, string> = {},
|
|
442
337
|
transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
|
|
443
338
|
) {
|
|
339
|
+
this.runner = new CliScriptRunner(provider.type);
|
|
444
340
|
this.provider = provider;
|
|
445
341
|
this.transportFactory = transportFactory;
|
|
446
342
|
this.cliType = provider.type;
|
|
@@ -458,10 +354,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
458
354
|
this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
|
|
459
355
|
this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
|
|
460
356
|
|
|
461
|
-
//
|
|
462
|
-
this.
|
|
463
|
-
|
|
464
|
-
|
|
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();
|
|
465
373
|
if (scriptNames.length > 0) {
|
|
466
374
|
LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
|
|
467
375
|
LOG.info(
|
|
@@ -487,14 +395,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
487
395
|
|
|
488
396
|
/** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
|
|
489
397
|
setCliScripts(scripts: CliScripts): void {
|
|
490
|
-
this.
|
|
398
|
+
this.runner.setScripts(scripts);
|
|
491
399
|
this.parsedStatusCache = null;
|
|
492
|
-
this.
|
|
493
|
-
// Initialize per-session state: createState() is called once here and on script reload.
|
|
494
|
-
// The returned object lives until the PTY exits (scriptState = null on exit).
|
|
495
|
-
this.scriptState = typeof scripts.createState === 'function' ? (scripts.createState() ?? null) : null;
|
|
496
|
-
const scriptNames = listCliScriptNames(scripts);
|
|
497
|
-
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
|
|
400
|
+
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
|
|
498
401
|
}
|
|
499
402
|
|
|
500
403
|
/** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
|
|
@@ -548,15 +451,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
548
451
|
});
|
|
549
452
|
|
|
550
453
|
LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
|
|
551
|
-
this.resetTraceSession();
|
|
552
|
-
this.recordTrace('spawn', {
|
|
553
|
-
shellCommand: spawnPlan.shellCmd,
|
|
554
|
-
shellArgs: spawnPlan.shellArgs,
|
|
555
|
-
cwd: spawnPlan.ptyOptions.cwd,
|
|
556
|
-
cols: spawnPlan.ptyOptions.cols,
|
|
557
|
-
rows: spawnPlan.ptyOptions.rows,
|
|
558
|
-
providerResolution: this.providerResolutionMeta,
|
|
559
|
-
});
|
|
560
454
|
|
|
561
455
|
try {
|
|
562
456
|
this.ptyProcess = this.transportFactory.spawn(
|
|
@@ -620,13 +514,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
620
514
|
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
621
515
|
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
|
|
622
516
|
this.flushPendingOutputParse();
|
|
623
|
-
this.recordTrace('exit', { exitCode });
|
|
624
517
|
this.ptyProcess = null;
|
|
625
|
-
this.
|
|
518
|
+
this.engine.onPtyExit();
|
|
626
519
|
this.ready = false;
|
|
627
520
|
this.startupParseGate = false;
|
|
628
521
|
this.spawnAt = 0;
|
|
629
|
-
this.
|
|
522
|
+
this.runner.resetSessionState();
|
|
630
523
|
this.onStatusChange?.();
|
|
631
524
|
});
|
|
632
525
|
|
|
@@ -637,15 +530,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
637
530
|
if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
|
|
638
531
|
this.resetTerminalScreen(24, 80);
|
|
639
532
|
this.pendingTerminalQueryTail = '';
|
|
640
|
-
this.currentTurnScope = null;
|
|
641
|
-
this.finishRetryCount = 0;
|
|
642
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
643
533
|
this.ready = false;
|
|
644
534
|
await this.ptyProcess.ready;
|
|
645
|
-
this.
|
|
646
|
-
runtimeMeta: this.getRuntimeMetadata(),
|
|
647
|
-
});
|
|
648
|
-
this.setStatus('starting', 'pty_ready');
|
|
535
|
+
this.engine.onSpawnReady();
|
|
649
536
|
this.scheduleStartupSettleCheck();
|
|
650
537
|
this.onStatusChange?.();
|
|
651
538
|
}
|
|
@@ -671,11 +558,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
671
558
|
if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
|
|
672
559
|
this.startupFirstOutputAt = now;
|
|
673
560
|
}
|
|
674
|
-
if (
|
|
675
|
-
this.clearIdleFinishCandidate('new_output');
|
|
561
|
+
if (rawData.length > 0 || cleanData.length > 0) {
|
|
562
|
+
this.engine.clearIdleFinishCandidate('new_output');
|
|
676
563
|
}
|
|
677
564
|
if (getDebugRuntimeConfig().collectDebugTrace) {
|
|
678
|
-
this.
|
|
565
|
+
this.engine.recordExternalTrace('output', {
|
|
679
566
|
rawLength: rawData.length,
|
|
680
567
|
cleanLength: cleanData.length,
|
|
681
568
|
rawPreview: summarizeCliTraceText(rawData, 300),
|
|
@@ -687,7 +574,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
687
574
|
this.scheduleStartupSettleCheck();
|
|
688
575
|
}
|
|
689
576
|
|
|
690
|
-
if (this.isWaitingForResponse && cleanData) {
|
|
577
|
+
if (this.engine.isWaitingForResponse && cleanData) {
|
|
691
578
|
const previousResponseLen = this.responseBuffer.length;
|
|
692
579
|
this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
|
|
693
580
|
this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
|
|
@@ -726,19 +613,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
726
613
|
// Keep turn-scope offsets aligned with the truncated buffer so scoped
|
|
727
614
|
// parses don't lose the beginning of a long turn (e.g. the Hermes
|
|
728
615
|
// ╭─ opening line) when the rolling window sheds bytes.
|
|
729
|
-
if (this.currentTurnScope) {
|
|
616
|
+
if (this.engine.currentTurnScope) {
|
|
730
617
|
if (droppedClean > 0) {
|
|
731
|
-
this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
|
|
618
|
+
this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
|
|
732
619
|
}
|
|
733
620
|
if (droppedRaw > 0) {
|
|
734
|
-
this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
|
|
621
|
+
this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
|
|
735
622
|
}
|
|
736
623
|
}
|
|
737
624
|
|
|
738
625
|
this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
|
|
739
626
|
|
|
740
627
|
// ─── Script-based status detection
|
|
741
|
-
this.scheduleSettle();
|
|
628
|
+
this.engine.scheduleSettle();
|
|
742
629
|
}
|
|
743
630
|
|
|
744
631
|
private resolveStartupState(
|
|
@@ -761,6 +648,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
761
648
|
if (stableMs < 2000) return;
|
|
762
649
|
|
|
763
650
|
const startupModal = this.runParseApproval(this.recentOutputBuffer);
|
|
651
|
+
const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
652
|
+
if (!startupModal && startupStatus !== 'idle') {
|
|
653
|
+
this.scheduleStartupSettleCheck();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
764
656
|
this.startupParseGate = false;
|
|
765
657
|
if (this.startupSettleTimer) {
|
|
766
658
|
clearTimeout(this.startupSettleTimer);
|
|
@@ -768,14 +660,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
768
660
|
}
|
|
769
661
|
this.ready = true;
|
|
770
662
|
if (startupModal) {
|
|
771
|
-
this.activeModal = startupModal;
|
|
772
|
-
this.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
663
|
+
this.engine.activeModal = startupModal;
|
|
664
|
+
this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
773
665
|
} else {
|
|
774
|
-
if (this.currentStatus === 'waiting_approval' || this.activeModal) {
|
|
775
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
666
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
|
|
667
|
+
this.engine.lastApprovalResolvedAt = Date.now();
|
|
776
668
|
}
|
|
777
|
-
this.activeModal = null;
|
|
778
|
-
|
|
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}`);
|
|
779
679
|
}
|
|
780
680
|
LOG.info(
|
|
781
681
|
'CLI',
|
|
@@ -801,88 +701,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
801
701
|
}, delayMs);
|
|
802
702
|
}
|
|
803
703
|
|
|
804
|
-
private scheduleSettle(): void {
|
|
805
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
806
|
-
const settleEpoch = this.responseEpoch;
|
|
807
|
-
const delay = Math.max(
|
|
808
|
-
this.timeouts.outputSettle,
|
|
809
|
-
this.submitPendingUntil > Date.now()
|
|
810
|
-
? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
|
|
811
|
-
: 0,
|
|
812
|
-
);
|
|
813
|
-
this.settleTimer = setTimeout(() => {
|
|
814
|
-
this.settleTimer = null;
|
|
815
|
-
if (settleEpoch !== this.responseEpoch) return;
|
|
816
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
817
|
-
this.evaluateSettled();
|
|
818
|
-
}, delay);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
private armApprovalExitTimeout(): void {
|
|
822
|
-
if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
|
|
823
|
-
this.approvalExitTimeout = setTimeout(() => {
|
|
824
|
-
if (!this.hasActionableApproval()) return;
|
|
825
|
-
const tail = this.recentOutputBuffer;
|
|
826
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
827
|
-
const modal = this.runParseApproval(tail);
|
|
828
|
-
const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
|
|
829
|
-
if (stillWaiting) {
|
|
830
|
-
if (!modal) {
|
|
831
|
-
LOG.warn('CLI', `[${this.cliType}] approval timeout check found no actionable modal; keeping approval state fail-closed`);
|
|
832
|
-
this.activeModal = null;
|
|
833
|
-
this.onStatusChange?.();
|
|
834
|
-
this.armApprovalExitTimeout();
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
this.activeModal = modal;
|
|
838
|
-
this.onStatusChange?.();
|
|
839
|
-
this.armApprovalExitTimeout();
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
|
|
843
|
-
this.activeModal = null;
|
|
844
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
845
|
-
this.setStatus('idle', 'approval_timeout');
|
|
846
|
-
this.onStatusChange?.();
|
|
847
|
-
}, 60000);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
private shouldRetryFinishResponse(commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
|
|
851
|
-
if (!this.currentTurnScope) return false;
|
|
852
|
-
if (this.hasActionableApproval()) return false;
|
|
853
|
-
if (this.finishRetryCount >= ProviderCliAdapter.MAX_FINISH_RETRIES) return false;
|
|
854
|
-
if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
|
|
855
|
-
|
|
856
|
-
if (this.runDetectStatus(this.recentOutputBuffer) !== 'idle') return false;
|
|
857
|
-
|
|
858
|
-
const now = Date.now();
|
|
859
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
860
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
861
|
-
return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
private hasRecentInteractiveActivity(now: number): boolean {
|
|
865
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
866
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : Number.MAX_SAFE_INTEGER;
|
|
867
|
-
const holdMs = this.getStatusActivityHoldMs();
|
|
868
|
-
return quietForMs < holdMs
|
|
869
|
-
|| screenStableMs < holdMs;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
private shouldDeferIdleTimeoutFinish(): boolean {
|
|
873
|
-
if (!this.isWaitingForResponse || this.hasActionableApproval()) {
|
|
874
|
-
return false;
|
|
875
|
-
}
|
|
876
|
-
const latestStatus = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
877
|
-
if (latestStatus === 'generating') {
|
|
878
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
879
|
-
this.evaluateSettled();
|
|
880
|
-
return true;
|
|
881
|
-
}
|
|
882
|
-
return false;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
|
|
886
704
|
private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
|
|
887
705
|
const startedAt = Date.now();
|
|
888
706
|
let loggedWait = false;
|
|
@@ -892,7 +710,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
892
710
|
const screenText = this.terminalScreen.getText() || '';
|
|
893
711
|
const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
|
|
894
712
|
const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
895
|
-
const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
713
|
+
const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
|
|
896
714
|
const interactiveReady = status === 'idle'
|
|
897
715
|
&& stableMs >= 700
|
|
898
716
|
&& recentlyOutput >= 350;
|
|
@@ -926,637 +744,84 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
926
744
|
|
|
927
745
|
private clearAllTimers(): void {
|
|
928
746
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
929
|
-
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
930
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
931
747
|
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
932
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
933
|
-
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
934
|
-
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
935
748
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
936
749
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
750
|
+
this.engine.clearAllTimers();
|
|
937
751
|
}
|
|
938
752
|
|
|
939
|
-
|
|
940
|
-
const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
|
|
941
|
-
const isIdle = this.runDetectStatus(this.recentOutputBuffer) === 'idle';
|
|
942
|
-
if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !isIdle || !!blockingModal) {
|
|
943
|
-
return false;
|
|
944
|
-
}
|
|
945
|
-
this.clearAllTimers();
|
|
946
|
-
this.clearIdleFinishCandidate(reason);
|
|
947
|
-
this.responseBuffer = '';
|
|
948
|
-
this.isWaitingForResponse = false;
|
|
949
|
-
this.responseSettleIgnoreUntil = 0;
|
|
950
|
-
this.submitRetryUsed = false;
|
|
951
|
-
this.submitRetryPromptSnippet = '';
|
|
952
|
-
this.finishRetryCount = 0;
|
|
953
|
-
this.currentTurnScope = null;
|
|
954
|
-
this.activeModal = null;
|
|
955
|
-
this.recordTrace('stale_idle_response_cleared', { reason });
|
|
956
|
-
return true;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
|
|
960
|
-
const raw = String(this.responseBuffer || '').trim();
|
|
961
|
-
if (!raw) return false;
|
|
962
|
-
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
963
|
-
if (!normalizedPrompt) return true;
|
|
964
|
-
const normalizedBuffer = compactPromptText(raw);
|
|
965
|
-
if (!normalizedBuffer) return false;
|
|
966
|
-
if (normalizedBuffer === normalizedPrompt) return false;
|
|
967
|
-
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
968
|
-
const remainder = normalizedBuffer
|
|
969
|
-
.slice(normalizedPrompt.length)
|
|
970
|
-
.replace(/[─═\-]+/g, '')
|
|
971
|
-
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
972
|
-
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
973
|
-
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
974
|
-
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
975
|
-
.replace(/esctointerrupt/gi, '')
|
|
976
|
-
.replace(/❯/g, '')
|
|
977
|
-
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
978
|
-
.trim();
|
|
979
|
-
return remainder.length > 0;
|
|
980
|
-
}
|
|
981
|
-
return true;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
private evaluateSettled(): void {
|
|
985
|
-
const now = Date.now();
|
|
986
|
-
if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
|
|
987
|
-
const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
|
|
988
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
989
|
-
this.settleTimer = setTimeout(() => {
|
|
990
|
-
this.settleTimer = null;
|
|
991
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
992
|
-
this.evaluateSettled();
|
|
993
|
-
}, delayTime);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
this.resolveStartupState('settled');
|
|
998
|
-
if (this.startupParseGate) return;
|
|
753
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
999
754
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
lastOutputAt: this.lastOutputAt,
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages)) return;
|
|
1022
|
-
|
|
1023
|
-
const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
|
|
1024
|
-
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || '');
|
|
1025
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1026
|
-
|
|
1027
|
-
this.recordTrace('settled', {
|
|
1028
|
-
tail: summarizeCliTraceText(this.settledBuffer, 500),
|
|
1029
|
-
screenText: summarizeCliTraceText(screenText, 1200),
|
|
1030
|
-
detectStatus: status,
|
|
1031
|
-
parsedStatus: parsedStatus || null,
|
|
1032
|
-
parsedMessageCount: parsedMessages.length,
|
|
1033
|
-
parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
|
|
1034
|
-
parsedActiveModal: modal,
|
|
1035
|
-
approval: modal,
|
|
1036
|
-
...buildCliTraceParseSnapshot({
|
|
1037
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1038
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1039
|
-
responseBuffer: this.responseBuffer,
|
|
1040
|
-
partialResponse: this.responseBuffer,
|
|
1041
|
-
scope: this.currentTurnScope,
|
|
1042
|
-
}),
|
|
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,
|
|
1043
773
|
});
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
&& !this.submitRetryUsed
|
|
1049
|
-
&& this.ptyProcess
|
|
1050
|
-
&& !this.hasActionableApproval()
|
|
1051
|
-
&& promptLikelyVisible(screenText, normalizedPromptSnippet)
|
|
1052
|
-
&& !this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)
|
|
1053
|
-
) {
|
|
1054
|
-
this.submitRetryUsed = true;
|
|
1055
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1056
|
-
LOG.info('CLI', `[${this.cliType}] Retrying submit key from settled parser (no assistant yet)`);
|
|
1057
|
-
this.recordTrace('submit_write', {
|
|
1058
|
-
mode: 'settled_retry',
|
|
1059
|
-
sendKey: this.sendKey,
|
|
1060
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
1061
|
-
});
|
|
1062
|
-
this.ptyProcess.write(this.sendKey);
|
|
1063
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
1064
|
-
this.settleTimer = setTimeout(() => {
|
|
1065
|
-
this.settleTimer = null;
|
|
1066
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1067
|
-
this.evaluateSettled();
|
|
1068
|
-
}, this.timeouts.outputSettle + 150);
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
if (this.currentTurnScope && !lastParsedAssistant) {
|
|
1073
|
-
LOG.debug(
|
|
1074
|
-
'CLI',
|
|
1075
|
-
`[${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 || '-'}`
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (!status) return;
|
|
1080
|
-
|
|
1081
|
-
const prevStatus = this.currentStatus;
|
|
1082
|
-
const ctx: SettledEvalContext = { now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus };
|
|
1083
|
-
|
|
1084
|
-
if (!this.applyPendingScriptStatusDebounce(ctx)) return;
|
|
1085
|
-
|
|
1086
|
-
const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
|
|
1087
|
-
LOG.debug(
|
|
1088
|
-
'CLI',
|
|
1089
|
-
`[${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)}`
|
|
1090
|
-
);
|
|
1091
|
-
|
|
1092
|
-
const shouldHoldGenerating =
|
|
1093
|
-
status === 'idle'
|
|
1094
|
-
&& this.isWaitingForResponse
|
|
1095
|
-
&& !modal
|
|
1096
|
-
&& recentInteractiveActivity
|
|
1097
|
-
&& !(parsedStatus === 'idle' && !!lastParsedAssistant);
|
|
1098
|
-
|
|
1099
|
-
if (shouldHoldGenerating) { this.applyHoldGenerating(ctx, recentInteractiveActivity); return; }
|
|
1100
|
-
if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
|
|
1101
|
-
if (status === 'generating') { this.applyGenerating(ctx); return; }
|
|
1102
|
-
if (status === 'idle') { this.applyIdle(ctx, now); }
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// Returns false if the caller should bail out (debounce pending).
|
|
1106
|
-
private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
|
|
1107
|
-
const { now, status, prevStatus } = ctx;
|
|
1108
|
-
const shouldDebounce =
|
|
1109
|
-
prevStatus === 'idle'
|
|
1110
|
-
&& !this.isWaitingForResponse
|
|
1111
|
-
&& !this.currentTurnScope
|
|
1112
|
-
&& (status === 'generating' || status === 'waiting_approval');
|
|
1113
|
-
|
|
1114
|
-
if (!shouldDebounce) {
|
|
1115
|
-
this.pendingScriptStatus = null;
|
|
1116
|
-
this.pendingScriptStatusSince = 0;
|
|
1117
|
-
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
1118
|
-
return true;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const armPending = (delayMs: number) => {
|
|
1122
|
-
if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
|
|
1123
|
-
this.pendingScriptStatusTimer = setTimeout(() => {
|
|
1124
|
-
this.pendingScriptStatusTimer = null;
|
|
1125
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
1126
|
-
this.evaluateSettled();
|
|
1127
|
-
}, delayMs);
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
|
-
if (this.pendingScriptStatus !== status) {
|
|
1131
|
-
this.pendingScriptStatus = status as 'generating' | 'waiting_approval';
|
|
1132
|
-
this.pendingScriptStatusSince = now;
|
|
1133
|
-
armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
|
|
1134
|
-
return false;
|
|
1135
|
-
}
|
|
1136
|
-
const elapsed = now - this.pendingScriptStatusSince;
|
|
1137
|
-
if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
|
|
1138
|
-
armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
|
|
1139
|
-
return false;
|
|
1140
|
-
}
|
|
1141
|
-
return true;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
private applyHoldGenerating(ctx: SettledEvalContext, recentInteractiveActivity: boolean): void {
|
|
1145
|
-
const { status } = ctx;
|
|
1146
|
-
this.clearIdleFinishCandidate('hold_generating_recent_activity');
|
|
1147
|
-
this.setStatus('generating', 'recent_activity_hold');
|
|
1148
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1149
|
-
this.idleTimeout = setTimeout(() => {
|
|
1150
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1151
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1152
|
-
this.finishResponse();
|
|
1153
|
-
}
|
|
1154
|
-
}, this.timeouts.generatingIdle);
|
|
1155
|
-
this.recordTrace('hold_generating_recent_activity', {
|
|
1156
|
-
scriptStatus: status,
|
|
1157
|
-
recentInteractiveActivity,
|
|
1158
|
-
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
1159
|
-
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1160
|
-
holdMs: this.getStatusActivityHoldMs(),
|
|
1161
|
-
...buildCliTraceParseSnapshot({
|
|
1162
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1163
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1164
|
-
responseBuffer: this.responseBuffer,
|
|
1165
|
-
partialResponse: this.responseBuffer,
|
|
1166
|
-
scope: this.currentTurnScope,
|
|
1167
|
-
}),
|
|
774
|
+
const session = this.runner.parseSession({
|
|
775
|
+
...input,
|
|
776
|
+
tail,
|
|
777
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1168
778
|
});
|
|
1169
|
-
this.
|
|
779
|
+
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
780
|
+
return session;
|
|
1170
781
|
}
|
|
1171
782
|
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
this.
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
this.idleTimeout = setTimeout(() => {
|
|
1183
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1184
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1185
|
-
this.finishResponse();
|
|
1186
|
-
}
|
|
1187
|
-
}, this.timeouts.generatingIdle);
|
|
1188
|
-
} else {
|
|
1189
|
-
this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
|
|
1190
|
-
}
|
|
1191
|
-
this.onStatusChange?.();
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
if (!inCooldown) {
|
|
1195
|
-
if (!modal) {
|
|
1196
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
this.isWaitingForResponse = true;
|
|
1200
|
-
this.setStatus('waiting_approval', 'script_detect');
|
|
1201
|
-
this.activeModal = modal;
|
|
1202
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1203
|
-
this.armApprovalExitTimeout();
|
|
1204
|
-
this.onStatusChange?.();
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
private applyGenerating(ctx: SettledEvalContext): void {
|
|
1209
|
-
const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
|
|
1210
|
-
this.clearIdleFinishCandidate('generating');
|
|
1211
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1212
|
-
const effectiveScreenText = screenText || this.accumulatedBuffer;
|
|
1213
|
-
const noActiveTurn = !this.currentTurnScope;
|
|
1214
|
-
const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
|
|
1215
|
-
const parsedShowsLiveAssistantProgress = parsedStatus === 'generating'
|
|
1216
|
-
&& !!lastParsedAssistant
|
|
1217
|
-
;
|
|
1218
|
-
if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (prevStatus === 'waiting_approval') {
|
|
1222
|
-
// Transitioned out of approval → generating
|
|
1223
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1224
|
-
this.activeModal = null;
|
|
1225
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1226
|
-
}
|
|
1227
|
-
if (!this.isWaitingForResponse) {
|
|
1228
|
-
this.isWaitingForResponse = true;
|
|
1229
|
-
this.responseBuffer = '';
|
|
1230
|
-
}
|
|
1231
|
-
this.setStatus('generating', 'script_detect');
|
|
1232
|
-
// Reset idle timeout
|
|
1233
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1234
|
-
this.idleTimeout = setTimeout(() => {
|
|
1235
|
-
if (this.isWaitingForResponse) {
|
|
1236
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1237
|
-
this.finishResponse();
|
|
1238
|
-
}
|
|
1239
|
-
}, this.timeouts.generatingIdle);
|
|
1240
|
-
this.onStatusChange?.();
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
private applyIdle(ctx: SettledEvalContext, now: number): void {
|
|
1244
|
-
const { modal, lastParsedAssistant, prevStatus } = ctx;
|
|
1245
|
-
if (prevStatus === 'waiting_approval') {
|
|
1246
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1247
|
-
this.activeModal = null;
|
|
1248
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1249
|
-
this.setStatus('idle', 'approval_prompt_gone_script_idle');
|
|
1250
|
-
}
|
|
1251
|
-
if (!this.isWaitingForResponse) {
|
|
1252
|
-
if (prevStatus !== 'idle') {
|
|
1253
|
-
this.clearIdleFinishCandidate('idle_without_response');
|
|
1254
|
-
this.setStatus('idle', 'script_detect');
|
|
1255
|
-
this.onStatusChange?.();
|
|
1256
|
-
}
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
1260
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
1261
|
-
const hasAssistantTurn = !!lastParsedAssistant;
|
|
1262
|
-
const assistantLength = lastParsedAssistant?.content?.length || 0;
|
|
1263
|
-
const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
|
|
1264
|
-
const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
|
|
1265
|
-
const idleReady = !modal
|
|
1266
|
-
&& hasAssistantTurn
|
|
1267
|
-
&& quietForMs >= idleQuietThresholdMs
|
|
1268
|
-
&& screenStableMs >= idleFinishConfirmMs;
|
|
1269
|
-
const candidate = this.idleFinishCandidate;
|
|
1270
|
-
const candidateQuiet = !!candidate
|
|
1271
|
-
&& candidate.responseEpoch === this.responseEpoch
|
|
1272
|
-
&& candidate.lastOutputAt === this.lastOutputAt
|
|
1273
|
-
&& candidate.lastScreenChangeAt === this.lastScreenChangeAt
|
|
1274
|
-
&& assistantLength >= candidate.assistantLength
|
|
1275
|
-
&& (now - candidate.armedAt) >= idleFinishConfirmMs;
|
|
1276
|
-
|
|
1277
|
-
this.recordTrace('idle_decision', {
|
|
1278
|
-
quietForMs,
|
|
1279
|
-
screenStableMs,
|
|
1280
|
-
hasAssistantTurn,
|
|
1281
|
-
assistantLength,
|
|
1282
|
-
hasModal: !!modal,
|
|
1283
|
-
idleQuietThresholdMs,
|
|
1284
|
-
idleStableThresholdMs: idleFinishConfirmMs,
|
|
1285
|
-
idleReady,
|
|
1286
|
-
idleFinishConfirmMs,
|
|
1287
|
-
idleFinishCandidate: candidate,
|
|
1288
|
-
candidateQuiet,
|
|
1289
|
-
canFinishImmediately: idleReady && candidateQuiet,
|
|
1290
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
1291
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1292
|
-
...buildCliTraceParseSnapshot({
|
|
1293
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1294
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1295
|
-
responseBuffer: this.responseBuffer,
|
|
1296
|
-
partialResponse: this.responseBuffer,
|
|
1297
|
-
scope: this.currentTurnScope,
|
|
1298
|
-
}),
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
if (idleReady && candidateQuiet) {
|
|
1302
|
-
this.clearIdleFinishCandidate('finish_response');
|
|
1303
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1304
|
-
this.finishResponse();
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (idleReady) {
|
|
1309
|
-
if (!candidate) {
|
|
1310
|
-
this.armIdleFinishCandidate(assistantLength);
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
} else {
|
|
1314
|
-
this.clearIdleFinishCandidate('idle_not_ready');
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1318
|
-
this.idleTimeout = setTimeout(() => {
|
|
1319
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1320
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1321
|
-
this.clearIdleFinishCandidate('idle_timeout_finish');
|
|
1322
|
-
this.finishResponse();
|
|
1323
|
-
}
|
|
1324
|
-
}, this.timeouts.idleFinish);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
private finishResponse(): void {
|
|
1328
|
-
if (this.submitPendingUntil > Date.now()) return;
|
|
1329
|
-
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
1330
|
-
this.clearIdleFinishCandidate('finish_response_enter');
|
|
1331
|
-
this.recordTrace('finish_response', {
|
|
1332
|
-
...buildCliTraceParseSnapshot({
|
|
1333
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1334
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1335
|
-
responseBuffer: this.responseBuffer,
|
|
1336
|
-
partialResponse: this.responseBuffer,
|
|
1337
|
-
scope: this.currentTurnScope,
|
|
1338
|
-
}),
|
|
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),
|
|
1339
793
|
});
|
|
1340
|
-
const commitResult = this.commitCurrentTranscript();
|
|
1341
|
-
if (this.shouldRetryFinishResponse(commitResult)) {
|
|
1342
|
-
this.finishRetryCount += 1;
|
|
1343
|
-
this.recordTrace('finish_response_retry', {
|
|
1344
|
-
retryCount: this.finishRetryCount,
|
|
1345
|
-
retryDelayMs: ProviderCliAdapter.FINISH_RETRY_DELAY_MS,
|
|
1346
|
-
assistantContent: summarizeCliTraceText(commitResult.assistantContent, 220),
|
|
1347
|
-
...buildCliTraceParseSnapshot({
|
|
1348
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1349
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1350
|
-
responseBuffer: this.responseBuffer,
|
|
1351
|
-
partialResponse: this.responseBuffer,
|
|
1352
|
-
scope: this.currentTurnScope,
|
|
1353
|
-
}),
|
|
1354
|
-
});
|
|
1355
|
-
if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
|
|
1356
|
-
this.finishRetryTimer = setTimeout(() => {
|
|
1357
|
-
this.finishRetryTimer = null;
|
|
1358
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1359
|
-
this.finishResponse();
|
|
1360
|
-
}
|
|
1361
|
-
}, ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
this.clearAllTimers();
|
|
1365
|
-
this.responseBuffer = '';
|
|
1366
|
-
this.isWaitingForResponse = false;
|
|
1367
|
-
this.responseSettleIgnoreUntil = 0;
|
|
1368
|
-
this.submitRetryUsed = false;
|
|
1369
|
-
this.submitRetryPromptSnippet = '';
|
|
1370
|
-
this.finishRetryCount = 0;
|
|
1371
|
-
this.currentTurnScope = null;
|
|
1372
|
-
this.activeModal = null;
|
|
1373
|
-
this.setStatus('idle', 'response_finished');
|
|
1374
|
-
this.onStatusChange?.();
|
|
1375
794
|
}
|
|
1376
795
|
|
|
1377
|
-
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
return false;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
const visibleAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant' && m.content.trim());
|
|
1392
|
-
if (!visibleAssistant) return false;
|
|
1393
|
-
|
|
1394
|
-
this.clearAllTimers();
|
|
1395
|
-
this.responseBuffer = '';
|
|
1396
|
-
this.isWaitingForResponse = false;
|
|
1397
|
-
this.responseSettleIgnoreUntil = 0;
|
|
1398
|
-
this.submitRetryUsed = false;
|
|
1399
|
-
this.submitRetryPromptSnippet = '';
|
|
1400
|
-
this.finishRetryCount = 0;
|
|
1401
|
-
this.currentTurnScope = null;
|
|
1402
|
-
this.activeModal = null;
|
|
1403
|
-
this.setStatus('idle', 'script_idle_commit');
|
|
1404
|
-
this.onStatusChange?.();
|
|
1405
|
-
this.recordTrace('script_idle_commit', {
|
|
1406
|
-
messageCount: parsedMessages.length,
|
|
1407
|
-
lastAssistant: summarizeCliTraceText(visibleAssistant.content, 320),
|
|
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),
|
|
1408
807
|
});
|
|
1409
|
-
return true;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
private commitCurrentTranscript(): { hasAssistant: boolean; assistantContent: string } {
|
|
1413
|
-
const parsed = this.parseCurrentTranscript(
|
|
1414
|
-
[],
|
|
1415
|
-
this.responseBuffer,
|
|
1416
|
-
this.currentTurnScope,
|
|
1417
|
-
);
|
|
1418
|
-
if (parsed && Array.isArray(parsed.messages)) {
|
|
1419
|
-
const parsedMessages = normalizeCliParsedMessages(parsed.messages, {
|
|
1420
|
-
scope: null,
|
|
1421
|
-
lastOutputAt: this.lastOutputAt,
|
|
1422
|
-
});
|
|
1423
|
-
const lastAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
|
|
1424
|
-
if (this.currentTurnScope) {
|
|
1425
|
-
LOG.info(
|
|
1426
|
-
'CLI',
|
|
1427
|
-
`[${this.cliType}] commitCurrentTranscript parserMessages=${parsedMessages.length} finalLastAssistant=${JSON.stringify(summarizeCliTraceText(lastAssistant?.content || '', 220)).slice(0, 260)}`
|
|
1428
|
-
);
|
|
1429
|
-
}
|
|
1430
|
-
this.recordTrace('commit_transcript', {
|
|
1431
|
-
parsedStatus: parsed.status || null,
|
|
1432
|
-
messageCount: parsedMessages.length,
|
|
1433
|
-
lastAssistant: lastAssistant ? summarizeCliTraceText(lastAssistant.content, 320) : '',
|
|
1434
|
-
messages: summarizeCliTraceMessages(parsedMessages),
|
|
1435
|
-
...buildCliTraceParseSnapshot({
|
|
1436
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1437
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1438
|
-
responseBuffer: this.responseBuffer,
|
|
1439
|
-
partialResponse: this.responseBuffer,
|
|
1440
|
-
scope: this.currentTurnScope,
|
|
1441
|
-
}),
|
|
1442
|
-
});
|
|
1443
|
-
if (!lastAssistant && this.currentTurnScope) {
|
|
1444
|
-
LOG.warn(
|
|
1445
|
-
'CLI',
|
|
1446
|
-
`[${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 || '-'}`
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
const hasAssistant = !!lastAssistant;
|
|
1450
|
-
return {
|
|
1451
|
-
hasAssistant,
|
|
1452
|
-
assistantContent: lastAssistant?.content || '',
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
|
-
if (this.currentTurnScope) {
|
|
1456
|
-
LOG.info(
|
|
1457
|
-
'CLI',
|
|
1458
|
-
`[${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 || '-'}`
|
|
1459
|
-
);
|
|
1460
|
-
}
|
|
1461
|
-
return {
|
|
1462
|
-
hasAssistant: false,
|
|
1463
|
-
assistantContent: '',
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
// ─── Script Execution ──────────────────────────
|
|
1469
|
-
|
|
1470
|
-
private invokeCliScript<T>(script: Function, input: any): T {
|
|
1471
|
-
const hasStateFactory = typeof this.cliScripts?.createState === 'function';
|
|
1472
|
-
const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
|
|
1473
|
-
return expectsStateArgument
|
|
1474
|
-
? script(this.scriptState, input)
|
|
1475
|
-
: script(input);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
private runParseSession(): ParsedSession | null {
|
|
1479
|
-
if (typeof this.cliScripts?.parseSession !== 'function') {
|
|
1480
|
-
this.parseErrorMessage = `${this.cliType} parseSession unavailable`;
|
|
1481
|
-
return null;
|
|
1482
|
-
}
|
|
1483
|
-
try {
|
|
1484
|
-
const screenText = this.terminalScreen.getText();
|
|
1485
|
-
const parseScreenText = this.getParseScreenText(screenText);
|
|
1486
|
-
const tail = this.recentOutputBuffer.slice(-500);
|
|
1487
|
-
const input = buildCliParseInput({
|
|
1488
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1489
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1490
|
-
recentOutputBuffer: this.recentOutputBuffer,
|
|
1491
|
-
terminalScreenText: parseScreenText,
|
|
1492
|
-
baseMessages: [],
|
|
1493
|
-
partialResponse: this.responseBuffer,
|
|
1494
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1495
|
-
scope: this.currentTurnScope,
|
|
1496
|
-
runtimeSettings: this.runtimeSettings,
|
|
1497
|
-
});
|
|
1498
|
-
const session = this.invokeCliScript<ParsedSession | null>(
|
|
1499
|
-
this.cliScripts.parseSession,
|
|
1500
|
-
{ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
|
|
1501
|
-
);
|
|
1502
|
-
this.parseErrorMessage = null;
|
|
1503
|
-
return session && typeof session === 'object' ? session : null;
|
|
1504
|
-
} catch (e: any) {
|
|
1505
|
-
const message = e?.message || String(e);
|
|
1506
|
-
this.parseErrorMessage = message;
|
|
1507
|
-
LOG.warn('CLI', `[${this.cliType}] parseSession error: ${message}`);
|
|
1508
|
-
return null;
|
|
1509
|
-
}
|
|
1510
808
|
}
|
|
1511
809
|
|
|
1512
|
-
private
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
rawBuffer: this.accumulatedRawBuffer,
|
|
1520
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1521
|
-
screen: buildCliScreenSnapshot(screenText),
|
|
1522
|
-
tailScreen: buildCliScreenSnapshot(text.slice(-500)),
|
|
1523
|
-
});
|
|
1524
|
-
return status;
|
|
1525
|
-
} catch (e: any) {
|
|
1526
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
|
|
1527
|
-
return null;
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
1532
|
-
if (!this.cliScripts?.parseApproval) return null;
|
|
1533
|
-
try {
|
|
1534
|
-
const screenText = this.terminalScreen.getText();
|
|
1535
|
-
const buffer = screenText || this.accumulatedBuffer;
|
|
1536
|
-
return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
|
|
1537
|
-
buffer,
|
|
1538
|
-
screenText,
|
|
1539
|
-
rawBuffer: this.accumulatedRawBuffer,
|
|
1540
|
-
tail,
|
|
1541
|
-
screen: buildCliScreenSnapshot(screenText),
|
|
1542
|
-
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
1543
|
-
tailScreen: buildCliScreenSnapshot(tail),
|
|
1544
|
-
});
|
|
1545
|
-
} catch (e: any) {
|
|
1546
|
-
LOG.warn('CLI', `[${this.cliType}] parseApproval error: ${e.message}`);
|
|
1547
|
-
return null;
|
|
810
|
+
private applyParsedSessionMetadata(parsed: any): void {
|
|
811
|
+
const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
|
|
812
|
+
? parsed.providerSessionId.trim()
|
|
813
|
+
: '';
|
|
814
|
+
if (providerSessionId && providerSessionId !== this.providerSessionId) {
|
|
815
|
+
this.providerSessionId = providerSessionId;
|
|
816
|
+
this.updateRuntimeMeta({ providerSessionId });
|
|
1548
817
|
}
|
|
1549
818
|
}
|
|
1550
819
|
|
|
1551
|
-
private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
|
|
1552
|
-
return !!(startupModal || this.activeModal);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
820
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1556
821
|
if (this.parseErrorMessage) return 'error';
|
|
1557
|
-
if (this.
|
|
1558
|
-
if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
|
|
1559
|
-
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;
|
|
1560
825
|
}
|
|
1561
826
|
|
|
1562
827
|
// ─── Public API (CliAdapter) ───────────────────
|
|
@@ -1564,17 +829,73 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1564
829
|
getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
|
|
1565
830
|
const allowParse = options.allowParse !== false;
|
|
1566
831
|
const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
832
|
+
const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
|
|
833
|
+
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
834
|
+
: null;
|
|
1567
835
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1568
|
-
let effectiveModal = startupModal || this.activeModal;
|
|
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
|
+
// (fix) Only surface modals that have at least one non-empty
|
|
850
|
+
// button. Half-rendered approval frames produced
|
|
851
|
+
// { message, buttons: [] } briefly — auto-approve would then
|
|
852
|
+
// pick buttonIndex=-1 and resolveModal would write the
|
|
853
|
+
// provider's index-0 key into the prompt. Skip those frames
|
|
854
|
+
// and let a later evaluate find the complete modal.
|
|
855
|
+
const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
|
|
856
|
+
&& liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
|
|
857
|
+
if (liveModal && buttonsOk) {
|
|
858
|
+
effectiveModal = liveModal;
|
|
859
|
+
if (!this.engine.activeModal) this.engine.activeModal = liveModal;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
// Only surface waiting_approval when we ALSO have a concrete modal
|
|
864
|
+
// (message + buttons). detectStatus alone can fire while parseApproval
|
|
865
|
+
// is still null — the engine logs "detectStatus=waiting_approval but
|
|
866
|
+
// parseApproval returned null; ignoring". Without this guard getStatus
|
|
867
|
+
// was shipping a bare waiting_approval with activeModal=null and the
|
|
868
|
+
// user perceived the flow as broken.
|
|
869
|
+
if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
|
|
870
|
+
effectiveStatus = 'waiting_approval';
|
|
871
|
+
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
872
|
+
effectiveStatus = 'idle';
|
|
873
|
+
}
|
|
1569
874
|
if (allowParse && !startupModal && !effectiveModal) {
|
|
1570
875
|
const parsed = this.getFreshParsedStatusCache();
|
|
1571
876
|
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
1572
877
|
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
1573
878
|
? parsed.activeModal
|
|
1574
879
|
: null;
|
|
880
|
+
const hasFinalAssistant = (p: any) => {
|
|
881
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
882
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
883
|
+
};
|
|
1575
884
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1576
885
|
effectiveStatus = 'waiting_approval';
|
|
1577
886
|
effectiveModal = parsedModal;
|
|
887
|
+
} else if (
|
|
888
|
+
effectiveStatus === 'idle'
|
|
889
|
+
&& parsed?.status === 'generating'
|
|
890
|
+
&& !hasFinalAssistant(parsed)
|
|
891
|
+
) {
|
|
892
|
+
effectiveStatus = 'generating';
|
|
893
|
+
} else if (
|
|
894
|
+
effectiveStatus === 'generating'
|
|
895
|
+
&& parsed?.status === 'idle'
|
|
896
|
+
&& hasFinalAssistant(parsed)
|
|
897
|
+
) {
|
|
898
|
+
effectiveStatus = 'idle';
|
|
1578
899
|
}
|
|
1579
900
|
}
|
|
1580
901
|
const bufferState = this.getBufferState();
|
|
@@ -1583,8 +904,17 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1583
904
|
messages: [],
|
|
1584
905
|
workingDir: this.workingDir,
|
|
1585
906
|
activeModal: effectiveModal,
|
|
1586
|
-
|
|
1587
|
-
|
|
907
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
908
|
+
pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
|
|
909
|
+
id: message.id,
|
|
910
|
+
role: message.role,
|
|
911
|
+
content: message.content,
|
|
912
|
+
queuedAt: message.queuedAt,
|
|
913
|
+
source: message.source,
|
|
914
|
+
})),
|
|
915
|
+
errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
|
|
916
|
+
errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
|
|
917
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1588
918
|
...(bufferState ? { bufferState } : {}),
|
|
1589
919
|
};
|
|
1590
920
|
}
|
|
@@ -1600,15 +930,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1600
930
|
const cached = this.parsedStatusCache;
|
|
1601
931
|
const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
|
|
1602
932
|
if (
|
|
1603
|
-
|
|
933
|
+
!this.providerOwnsTranscript()
|
|
934
|
+
&& cached
|
|
1604
935
|
&& cached.responseBuffer === this.responseBuffer
|
|
1605
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
936
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1606
937
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1607
938
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1608
939
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1609
940
|
&& cached.screenText === parseScreenText
|
|
1610
|
-
&& cached.currentStatus === this.currentStatus
|
|
1611
|
-
&& cached.activeModal === this.activeModal
|
|
941
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
942
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1612
943
|
&& cached.cliName === this.cliName
|
|
1613
944
|
) {
|
|
1614
945
|
return cached.result;
|
|
@@ -1623,14 +954,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1623
954
|
const bufferState = this.getBufferState();
|
|
1624
955
|
const result = {
|
|
1625
956
|
id: (parsed as any).id || 'cli_session',
|
|
1626
|
-
status: parsed.status || this.currentStatus,
|
|
957
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1627
958
|
title: (parsed as any).title || this.cliName,
|
|
1628
959
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1629
960
|
scope: null,
|
|
1630
961
|
lastOutputAt: this.lastOutputAt,
|
|
1631
962
|
}),
|
|
1632
963
|
activeModal,
|
|
1633
|
-
providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
|
|
964
|
+
providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
|
|
965
|
+
errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
|
|
966
|
+
? (parsed as any).errorMessage.trim()
|
|
967
|
+
: undefined,
|
|
968
|
+
errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
|
|
969
|
+
? (parsed as any).errorReason.trim()
|
|
970
|
+
: undefined,
|
|
1634
971
|
...(bufferState ? { bufferState } : {}),
|
|
1635
972
|
...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
|
|
1636
973
|
? { transcriptAuthority: (parsed as any).transcriptAuthority }
|
|
@@ -1642,13 +979,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1642
979
|
|
|
1643
980
|
this.parsedStatusCache = {
|
|
1644
981
|
responseBuffer: this.responseBuffer,
|
|
1645
|
-
currentTurnScope: this.currentTurnScope,
|
|
982
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1646
983
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1647
984
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1648
985
|
accumulatedRawBufferKey,
|
|
1649
986
|
screenText: parseScreenText,
|
|
1650
|
-
currentStatus: this.currentStatus,
|
|
1651
|
-
activeModal: this.activeModal,
|
|
987
|
+
currentStatus: this.engine.currentStatus,
|
|
988
|
+
activeModal: this.engine.activeModal,
|
|
1652
989
|
cliName: this.cliName,
|
|
1653
990
|
result,
|
|
1654
991
|
};
|
|
@@ -1656,22 +993,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1656
993
|
}
|
|
1657
994
|
|
|
1658
995
|
async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
|
|
1659
|
-
const fn = this.cliScripts?.[scriptName];
|
|
1660
|
-
if (typeof fn !== 'function') {
|
|
1661
|
-
throw new Error(`CLI script '${scriptName}' not available`);
|
|
1662
|
-
}
|
|
1663
996
|
const input = buildCliParseInput({
|
|
1664
997
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1665
998
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1666
999
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1667
1000
|
terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
|
|
1001
|
+
workingDir: this.workingDir,
|
|
1002
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1003
|
+
historySessionId: this.providerSessionId || undefined,
|
|
1668
1004
|
baseMessages: [],
|
|
1669
1005
|
partialResponse: this.responseBuffer,
|
|
1670
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1671
|
-
scope: this.currentTurnScope,
|
|
1006
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1007
|
+
scope: this.engine.currentTurnScope,
|
|
1672
1008
|
runtimeSettings: this.runtimeSettings,
|
|
1009
|
+
spawnAt: this.spawnAt,
|
|
1673
1010
|
});
|
|
1674
|
-
return await Promise.resolve(this.
|
|
1011
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1675
1012
|
...input,
|
|
1676
1013
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1677
1014
|
}));
|
|
@@ -1683,7 +1020,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1683
1020
|
|
|
1684
1021
|
/** Whether this adapter has CLI scripts loaded */
|
|
1685
1022
|
hasCliScripts(): boolean {
|
|
1686
|
-
return
|
|
1023
|
+
return this.runner.hasDetectStatus();
|
|
1687
1024
|
}
|
|
1688
1025
|
|
|
1689
1026
|
/**
|
|
@@ -1692,24 +1029,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1692
1029
|
*/
|
|
1693
1030
|
async resolveAction(data: any): Promise<void> {
|
|
1694
1031
|
let promptText = '';
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1032
|
+
try {
|
|
1033
|
+
promptText = this.runner.invokeByName('resolveAction', data);
|
|
1034
|
+
} catch {
|
|
1035
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
|
|
1036
|
+
return;
|
|
1701
1037
|
}
|
|
1702
1038
|
if (!promptText) {
|
|
1703
|
-
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not
|
|
1039
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
|
|
1704
1040
|
return;
|
|
1705
1041
|
}
|
|
1706
1042
|
await this.sendMessage(promptText);
|
|
1707
1043
|
}
|
|
1708
1044
|
|
|
1709
1045
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
1710
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
1711
|
-
if (this.hasActionableApproval()) return false;
|
|
1712
|
-
|
|
1046
|
+
if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
|
|
1047
|
+
if (this.engine.hasActionableApproval()) return false;
|
|
1048
|
+
// If there's already meaningful response content beyond the echoed prompt, not stuck
|
|
1049
|
+
if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
|
|
1713
1050
|
const screenText = this.terminalScreen.getText();
|
|
1714
1051
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
1715
1052
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -1718,20 +1055,41 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1718
1055
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
1719
1056
|
}
|
|
1720
1057
|
|
|
1058
|
+
private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
|
|
1059
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
1060
|
+
if (!raw) return false;
|
|
1061
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1062
|
+
if (!normalizedPrompt) return true;
|
|
1063
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
1064
|
+
if (!normalizedBuffer) return false;
|
|
1065
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1066
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1067
|
+
const remainder = normalizedBuffer
|
|
1068
|
+
.slice(normalizedPrompt.length)
|
|
1069
|
+
.replace(/[─═\-]+/g, '')
|
|
1070
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1071
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1072
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1073
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1074
|
+
.replace(/esctointerrupt/gi, '')
|
|
1075
|
+
.replace(/❯/g, '')
|
|
1076
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1077
|
+
.trim();
|
|
1078
|
+
return remainder.length > 0;
|
|
1079
|
+
}
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1721
1083
|
private async writeToPty(data: string): Promise<void> {
|
|
1722
1084
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1723
1085
|
await this.ptyProcess.write(data);
|
|
1724
1086
|
}
|
|
1725
1087
|
|
|
1726
1088
|
private resetPendingSendState(reason: string): void {
|
|
1727
|
-
this.isWaitingForResponse = false;
|
|
1728
1089
|
this.responseBuffer = '';
|
|
1729
|
-
this.currentTurnScope = null;
|
|
1730
|
-
this.submitPendingUntil = 0;
|
|
1731
|
-
this.clearIdleFinishCandidate(reason);
|
|
1732
1090
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1733
|
-
|
|
1734
|
-
|
|
1091
|
+
this.engine.resetActiveTurnState();
|
|
1092
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
1735
1093
|
}
|
|
1736
1094
|
|
|
1737
1095
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
@@ -1748,42 +1106,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1748
1106
|
}
|
|
1749
1107
|
this.responseTimeout = setTimeout(() => {
|
|
1750
1108
|
this.responseTimeout = null;
|
|
1751
|
-
if (!this.isWaitingForResponse) return;
|
|
1752
|
-
|
|
1753
|
-
const detectedStatusBeforeEval = this.runDetectStatus(this.recentOutputBuffer);
|
|
1754
|
-
this.recordTrace('response_timeout_check', {
|
|
1755
|
-
timeoutMs,
|
|
1756
|
-
detectedStatus: detectedStatusBeforeEval,
|
|
1757
|
-
currentStatus: this.currentStatus,
|
|
1758
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1759
|
-
hasActionableApproval: this.hasActionableApproval(),
|
|
1760
|
-
...buildCliTraceParseSnapshot({
|
|
1761
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1762
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1763
|
-
responseBuffer: this.responseBuffer,
|
|
1764
|
-
partialResponse: this.responseBuffer,
|
|
1765
|
-
scope: this.currentTurnScope,
|
|
1766
|
-
}),
|
|
1767
|
-
});
|
|
1109
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
1768
1110
|
|
|
1769
|
-
// maxResponse is a watchdog/checkpoint, not a completion signal.
|
|
1770
|
-
// behavior called finishResponse() unconditionally at the default 300s,
|
|
1771
|
-
// which fabricated idle transitions and downstream generating_completed
|
|
1772
|
-
// notifications while long-running CLIs were still generating. Re-run the
|
|
1111
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
1773
1112
|
// normal settled parser instead and keep the turn open unless the provider
|
|
1774
1113
|
// actually reports an idle, commit-ready state.
|
|
1775
|
-
this.
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1779
|
-
const detectedStatusAfterEval = this.runDetectStatus(this.recentOutputBuffer);
|
|
1780
|
-
this.recordTrace('response_timeout_kept_open', {
|
|
1781
|
-
timeoutMs,
|
|
1782
|
-
detectedStatusBeforeEval,
|
|
1783
|
-
detectedStatusAfterEval,
|
|
1784
|
-
currentStatus: this.currentStatus,
|
|
1785
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1786
|
-
});
|
|
1114
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1115
|
+
|
|
1116
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
1787
1117
|
this.armResponseTimeout();
|
|
1788
1118
|
}
|
|
1789
1119
|
}, timeoutMs);
|
|
@@ -1798,34 +1128,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1798
1128
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
1799
1129
|
this.submitRetryTimer = null;
|
|
1800
1130
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1801
|
-
|
|
1802
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1131
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1803
1132
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
|
|
1804
|
-
this.recordTrace('submit_write', {
|
|
1805
|
-
mode: 'submit_retry',
|
|
1806
|
-
attempt,
|
|
1807
|
-
sendKey: this.sendKey,
|
|
1808
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
1809
|
-
});
|
|
1810
1133
|
this.writeSubmitKeyForRetry('submit_retry');
|
|
1811
|
-
if (attempt >= 3) { this.submitRetryUsed = true; return; }
|
|
1134
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
1812
1135
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
1813
1136
|
}
|
|
1814
1137
|
|
|
1815
1138
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
1816
1139
|
this.submitRetryTimer = null;
|
|
1817
1140
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1818
|
-
|
|
1819
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1141
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1820
1142
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
|
|
1821
|
-
this.recordTrace('submit_write', {
|
|
1822
|
-
mode: 'immediate_retry',
|
|
1823
|
-
attempt: 1,
|
|
1824
|
-
sendKey: this.sendKey,
|
|
1825
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
1826
|
-
});
|
|
1827
1143
|
this.writeSubmitKeyForRetry('immediate_retry');
|
|
1828
|
-
this.submitRetryUsed = true;
|
|
1144
|
+
this.engine.submitRetryUsed = true;
|
|
1829
1145
|
}
|
|
1830
1146
|
|
|
1831
1147
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -1833,13 +1149,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1833
1149
|
completion.resolveOnce();
|
|
1834
1150
|
return;
|
|
1835
1151
|
}
|
|
1836
|
-
this.submitPendingUntil = 0;
|
|
1837
|
-
const screenText = this.terminalScreen.getText();
|
|
1838
|
-
this.recordTrace('submit_write', {
|
|
1839
|
-
mode: 'submit_key',
|
|
1840
|
-
sendKey: this.sendKey,
|
|
1841
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
1842
|
-
});
|
|
1152
|
+
this.engine.submitPendingUntil = 0;
|
|
1843
1153
|
void this.writeToPty(this.sendKey).then(() => {
|
|
1844
1154
|
this.commitSendUserTurn(state);
|
|
1845
1155
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -1849,13 +1159,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1849
1159
|
}
|
|
1850
1160
|
|
|
1851
1161
|
private submitImmediatePrompt(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
1852
|
-
this.submitPendingUntil = 0;
|
|
1853
|
-
this.recordTrace('submit_write', {
|
|
1854
|
-
mode: 'immediate',
|
|
1855
|
-
text: summarizeCliTraceText(state.text, 500),
|
|
1856
|
-
sendKey: this.sendKey,
|
|
1857
|
-
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
|
|
1858
|
-
});
|
|
1162
|
+
this.engine.submitPendingUntil = 0;
|
|
1859
1163
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
1860
1164
|
this.commitSendUserTurn(state);
|
|
1861
1165
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -1899,7 +1203,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1899
1203
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
1900
1204
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
1901
1205
|
};
|
|
1902
|
-
|
|
1206
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1207
|
+
|
|
1903
1208
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
1904
1209
|
// At this point the prompt text write already completed. Rejecting without
|
|
1905
1210
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -1924,12 +1229,110 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1924
1229
|
), 50);
|
|
1925
1230
|
}
|
|
1926
1231
|
|
|
1927
|
-
async sendMessage(text: string): Promise<void> {
|
|
1232
|
+
async sendMessage(text: string, options: { force?: boolean } = {}): Promise<void> {
|
|
1233
|
+
if (options.force === true) {
|
|
1234
|
+
await this.forceSendMessage(text);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
await this.sendMessageNow(text, true);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async forceSendMessage(text: string): Promise<void> {
|
|
1241
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1242
|
+
const content = String(text || '');
|
|
1243
|
+
if (!content.trim()) return;
|
|
1244
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
|
|
1245
|
+
await this.writeToPty(content + this.sendKey);
|
|
1246
|
+
this.onStatusChange?.();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
private enqueuePendingOutboundMessage(text: string, reason: string): void {
|
|
1250
|
+
const content = String(text || '');
|
|
1251
|
+
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
1252
|
+
if (duplicate) {
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const queuedAt = Date.now();
|
|
1256
|
+
const message: PendingOutboundMessage = {
|
|
1257
|
+
id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
|
|
1258
|
+
role: 'user',
|
|
1259
|
+
content,
|
|
1260
|
+
queuedAt,
|
|
1261
|
+
source: 'sendMessage',
|
|
1262
|
+
};
|
|
1263
|
+
this.pendingOutboundQueue.push(message);
|
|
1264
|
+
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
1265
|
+
this.onStatusChange?.();
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
1269
|
+
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
1270
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
1271
|
+
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1272
|
+
? String(parsedStatusBeforeSend.status)
|
|
1273
|
+
: '';
|
|
1274
|
+
const hasFinalAssistant = (p: any) => {
|
|
1275
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1276
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1277
|
+
};
|
|
1278
|
+
if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
|
|
1279
|
+
if (this.engine.currentStatus === 'generating') return 'current_status_generating';
|
|
1280
|
+
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
|
|
1281
|
+
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1282
|
+
const parsedHasActionableModal = Boolean(
|
|
1283
|
+
parsedModal
|
|
1284
|
+
&& Array.isArray(parsedModal.buttons)
|
|
1285
|
+
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1286
|
+
);
|
|
1287
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1288
|
+
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1289
|
+
&& !this.engine.isWaitingForResponse
|
|
1290
|
+
&& !this.engine.currentTurnScope
|
|
1291
|
+
&& !this.engine.hasActionableApproval()
|
|
1292
|
+
&& !parsedHasActionableModal;
|
|
1293
|
+
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
1294
|
+
}
|
|
1295
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
private schedulePendingOutboundFlush(delayMs = 0): void {
|
|
1300
|
+
if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
|
|
1301
|
+
this.pendingOutboundFlushTimer = setTimeout(() => {
|
|
1302
|
+
this.pendingOutboundFlushTimer = null;
|
|
1303
|
+
void this.flushPendingOutboundQueue();
|
|
1304
|
+
}, Math.max(0, delayMs));
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
private async flushPendingOutboundQueue(): Promise<void> {
|
|
1308
|
+
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
1309
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
1310
|
+
this.pendingOutboundFlushInFlight = true;
|
|
1311
|
+
try {
|
|
1312
|
+
while (this.pendingOutboundQueue.length > 0) {
|
|
1313
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
1314
|
+
const next = this.pendingOutboundQueue[0];
|
|
1315
|
+
try {
|
|
1316
|
+
await this.sendMessageNow(next.content, false);
|
|
1317
|
+
this.pendingOutboundQueue.shift();
|
|
1318
|
+
this.onStatusChange?.();
|
|
1319
|
+
} catch (error: any) {
|
|
1320
|
+
LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
|
|
1321
|
+
this.schedulePendingOutboundFlush(1000);
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
} finally {
|
|
1326
|
+
this.pendingOutboundFlushInFlight = false;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
|
|
1928
1331
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1929
1332
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
1930
1333
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
1931
|
-
&& this.isWaitingForResponse
|
|
1932
|
-
&& !this.hasActionableApproval();
|
|
1334
|
+
&& this.engine.isWaitingForResponse
|
|
1335
|
+
&& !this.engine.hasActionableApproval();
|
|
1933
1336
|
if (this.startupParseGate) {
|
|
1934
1337
|
const deadline = Date.now() + 10000;
|
|
1935
1338
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -1937,27 +1340,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1937
1340
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1938
1341
|
}
|
|
1939
1342
|
}
|
|
1343
|
+
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
1344
|
+
? (() => {
|
|
1345
|
+
try {
|
|
1346
|
+
return this.getScriptParsedStatus?.() || null;
|
|
1347
|
+
} catch {
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
})()
|
|
1351
|
+
: null;
|
|
1352
|
+
const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
|
|
1353
|
+
if (allowQueue && queueReason) {
|
|
1354
|
+
this.enqueuePendingOutboundMessage(text, queueReason);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1940
1357
|
if (!allowInterventionPrompt) {
|
|
1941
1358
|
await this.waitForInteractivePrompt();
|
|
1942
1359
|
}
|
|
1943
1360
|
if (!this.ready) {
|
|
1944
1361
|
this.resolveStartupState('send_precheck');
|
|
1945
|
-
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1362
|
+
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
1946
1363
|
this.ready = true;
|
|
1947
1364
|
this.startupParseGate = false;
|
|
1365
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1948
1366
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
1949
1367
|
}
|
|
1950
1368
|
}
|
|
1951
|
-
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
|
|
1952
|
-
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
1953
|
-
? (() => {
|
|
1954
|
-
try {
|
|
1955
|
-
return this.getScriptParsedStatus?.() || null;
|
|
1956
|
-
} catch {
|
|
1957
|
-
return null;
|
|
1958
|
-
}
|
|
1959
|
-
})()
|
|
1960
|
-
: null;
|
|
1369
|
+
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
1961
1370
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1962
1371
|
? String(parsedStatusBeforeSend.status)
|
|
1963
1372
|
: '';
|
|
@@ -1968,45 +1377,48 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1968
1377
|
&& Array.isArray(parsedModal.buttons)
|
|
1969
1378
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1970
1379
|
);
|
|
1971
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1380
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1972
1381
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1973
|
-
&& !this.isWaitingForResponse
|
|
1974
|
-
&& !this.currentTurnScope
|
|
1975
|
-
&& !this.hasActionableApproval()
|
|
1382
|
+
&& !this.engine.isWaitingForResponse
|
|
1383
|
+
&& !this.engine.currentTurnScope
|
|
1384
|
+
&& !this.engine.hasActionableApproval()
|
|
1976
1385
|
&& !parsedHasActionableModal;
|
|
1977
1386
|
if (!terminalLooksIdle) {
|
|
1387
|
+
if (allowQueue) {
|
|
1388
|
+
this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1978
1391
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1979
1392
|
}
|
|
1980
1393
|
}
|
|
1981
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1982
|
-
|
|
1394
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1395
|
+
const snap = this.getSnapshot();
|
|
1396
|
+
if (
|
|
1397
|
+
!this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
|
|
1398
|
+
&& !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
|
|
1399
|
+
) {
|
|
1400
|
+
if (allowQueue) {
|
|
1401
|
+
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1983
1404
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1984
1405
|
}
|
|
1985
1406
|
}
|
|
1986
|
-
this.isWaitingForResponse = true;
|
|
1987
1407
|
this.responseBuffer = '';
|
|
1988
|
-
|
|
1989
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1990
|
-
this.clearIdleFinishCandidate('send_message');
|
|
1991
|
-
this.currentTurnScope = {
|
|
1408
|
+
const turnScope: TurnParseScope = {
|
|
1992
1409
|
prompt: text,
|
|
1993
1410
|
startedAt: Date.now(),
|
|
1994
1411
|
bufferStart: this.accumulatedBuffer.length,
|
|
1995
1412
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
1996
1413
|
};
|
|
1997
|
-
|
|
1998
|
-
text: summarizeCliTraceText(text, 500),
|
|
1999
|
-
estimatedLines: estimatePromptDisplayLines(text),
|
|
2000
|
-
turnScope: this.currentTurnScope,
|
|
2001
|
-
});
|
|
2002
|
-
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2003
|
-
this.submitRetryUsed = false;
|
|
2004
|
-
this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
2005
|
-
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
|
|
1414
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2006
1415
|
if (this.submitRetryTimer) {
|
|
2007
1416
|
clearTimeout(this.submitRetryTimer);
|
|
2008
1417
|
this.submitRetryTimer = null;
|
|
2009
1418
|
}
|
|
1419
|
+
this.engine.onTurnStarted(turnScope);
|
|
1420
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1421
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2010
1422
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2011
1423
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2012
1424
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2019,12 +1431,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2019
1431
|
retryDelayMs,
|
|
2020
1432
|
didCommitUserTurn: false,
|
|
2021
1433
|
};
|
|
2022
|
-
|
|
2023
|
-
clearTimeout(this.settleTimer);
|
|
2024
|
-
this.settleTimer = null;
|
|
2025
|
-
}
|
|
2026
|
-
this.responseEpoch += 1;
|
|
2027
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1434
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2028
1435
|
await new Promise<void>((resolve, reject) => {
|
|
2029
1436
|
let resolved = false;
|
|
2030
1437
|
const completion: SendMessageCompletion = {
|
|
@@ -2047,24 +1454,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2047
1454
|
}
|
|
2048
1455
|
|
|
2049
1456
|
if (submitDelayMs > 0) {
|
|
2050
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1457
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2051
1458
|
}
|
|
2052
|
-
this.recordTrace('submit_write', {
|
|
2053
|
-
mode: 'type_then_submit',
|
|
2054
|
-
text: summarizeCliTraceText(text, 500),
|
|
2055
|
-
sendKey: this.sendKey,
|
|
2056
|
-
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
|
|
2057
|
-
});
|
|
2058
1459
|
const submitStartedAt = Date.now();
|
|
2059
1460
|
void this.writeToPty(text).then(
|
|
2060
1461
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2061
1462
|
completion.rejectOnce,
|
|
2062
1463
|
);
|
|
2063
1464
|
});
|
|
1465
|
+
// Schedule settle after successful send
|
|
1466
|
+
this.engine.scheduleSettle();
|
|
2064
1467
|
}
|
|
2065
1468
|
|
|
2066
1469
|
getPartialResponse(): string {
|
|
2067
|
-
if (!this.isWaitingForResponse) return '';
|
|
1470
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2068
1471
|
return this.responseBuffer;
|
|
2069
1472
|
}
|
|
2070
1473
|
|
|
@@ -2077,10 +1480,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2077
1480
|
cliType: this.cliType,
|
|
2078
1481
|
cliName: this.cliName,
|
|
2079
1482
|
workingDir: this.workingDir,
|
|
2080
|
-
currentStatus: this.currentStatus,
|
|
1483
|
+
currentStatus: this.engine.currentStatus,
|
|
2081
1484
|
ready: this.ready,
|
|
2082
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2083
|
-
activeModal: this.activeModal,
|
|
1485
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1486
|
+
activeModal: this.engine.activeModal,
|
|
2084
1487
|
parseErrorMessage: this.parseErrorMessage,
|
|
2085
1488
|
messageCounts: {
|
|
2086
1489
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2106,10 +1509,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2106
1509
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2107
1510
|
},
|
|
2108
1511
|
parser: {
|
|
2109
|
-
scriptNames:
|
|
2110
|
-
traceSessionId: this.
|
|
2111
|
-
traceSeq: this.
|
|
2112
|
-
currentTurnScope: this.currentTurnScope,
|
|
1512
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1513
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1514
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1515
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2113
1516
|
parsedStatusCache: parsedResult
|
|
2114
1517
|
? {
|
|
2115
1518
|
id: parsedResult.id,
|
|
@@ -2122,26 +1525,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2122
1525
|
activeModal: parsedResult.activeModal,
|
|
2123
1526
|
}
|
|
2124
1527
|
: null,
|
|
2125
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2126
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1528
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1529
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2127
1530
|
},
|
|
2128
1531
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2129
|
-
statusHistory: this.
|
|
2130
|
-
traceEntries: this.
|
|
1532
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1533
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2131
1534
|
timing: {
|
|
2132
1535
|
spawnAt: this.spawnAt,
|
|
2133
1536
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2134
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2135
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2136
|
-
responseEpoch: this.responseEpoch,
|
|
1537
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1538
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1539
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2137
1540
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2138
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1541
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2139
1542
|
},
|
|
2140
1543
|
finish: {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1544
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1545
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1546
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2145
1547
|
},
|
|
2146
1548
|
};
|
|
2147
1549
|
}
|
|
@@ -2152,6 +1554,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2152
1554
|
}
|
|
2153
1555
|
|
|
2154
1556
|
updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
|
|
1557
|
+
const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
|
|
1558
|
+
? meta.providerSessionId.trim()
|
|
1559
|
+
: '';
|
|
1560
|
+
if (nextProviderSessionId) {
|
|
1561
|
+
this.providerSessionId = nextProviderSessionId;
|
|
1562
|
+
}
|
|
2155
1563
|
if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
|
|
2156
1564
|
this.ptyProcess.updateMeta(meta, replace);
|
|
2157
1565
|
}
|
|
@@ -2172,7 +1580,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2172
1580
|
this.timeouts.shutdownGrace,
|
|
2173
1581
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2174
1582
|
);
|
|
2175
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1583
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2176
1584
|
|
|
2177
1585
|
try {
|
|
2178
1586
|
if (wasProcessing) {
|
|
@@ -2210,7 +1618,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2210
1618
|
return new Promise((resolve) => {
|
|
2211
1619
|
const startedAt = Date.now();
|
|
2212
1620
|
const timer = setInterval(() => {
|
|
2213
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
1621
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2214
1622
|
clearInterval(timer);
|
|
2215
1623
|
resolve(true);
|
|
2216
1624
|
return;
|
|
@@ -2224,18 +1632,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2224
1632
|
}
|
|
2225
1633
|
|
|
2226
1634
|
shutdown(): void {
|
|
2227
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
1635
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2228
1636
|
this.clearAllTimers();
|
|
2229
1637
|
this.pendingOutputParseChunks = [];
|
|
2230
1638
|
this.pendingTerminalQueryTail = '';
|
|
2231
1639
|
this.ptyOutputChunks = [];
|
|
2232
|
-
this.
|
|
1640
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1641
|
+
this.pendingOutboundQueue = [];
|
|
1642
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2233
1643
|
if (this.ptyProcess) {
|
|
2234
1644
|
this.ptyProcess.write('\x03');
|
|
2235
1645
|
setTimeout(() => {
|
|
2236
1646
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2237
1647
|
this.ptyProcess = null;
|
|
2238
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
1648
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2239
1649
|
this.ready = false;
|
|
2240
1650
|
this.startupParseGate = false;
|
|
2241
1651
|
this.spawnAt = 0;
|
|
@@ -2245,12 +1655,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2245
1655
|
}
|
|
2246
1656
|
|
|
2247
1657
|
detach(): void {
|
|
2248
|
-
this.clearIdleFinishCandidate('detach');
|
|
1658
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2249
1659
|
this.clearAllTimers();
|
|
2250
1660
|
this.pendingOutputParseChunks = [];
|
|
2251
1661
|
this.pendingTerminalQueryTail = '';
|
|
2252
1662
|
this.ptyOutputChunks = [];
|
|
2253
|
-
this.
|
|
1663
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1664
|
+
this.pendingOutboundQueue = [];
|
|
1665
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2254
1666
|
if (this.ptyProcess) {
|
|
2255
1667
|
try {
|
|
2256
1668
|
if (typeof this.ptyProcess.detach === 'function') {
|
|
@@ -2268,82 +1680,128 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2268
1680
|
}
|
|
2269
1681
|
|
|
2270
1682
|
clearHistory(): void {
|
|
2271
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
1683
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2272
1684
|
this.accumulatedBuffer = '';
|
|
2273
1685
|
this.accumulatedRawBuffer = '';
|
|
2274
|
-
this.currentTurnScope = null;
|
|
2275
|
-
this.submitRetryUsed = false;
|
|
2276
|
-
this.submitRetryPromptSnippet = '';
|
|
1686
|
+
this.engine.currentTurnScope = null;
|
|
1687
|
+
this.engine.submitRetryUsed = false;
|
|
1688
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2277
1689
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2278
1690
|
this.pendingOutputParseChunks = [];
|
|
2279
1691
|
this.pendingTerminalQueryTail = '';
|
|
2280
1692
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2281
1693
|
this.ptyOutputChunks = [];
|
|
2282
|
-
if (this.
|
|
2283
|
-
this.
|
|
1694
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1695
|
+
this.pendingOutboundQueue = [];
|
|
1696
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2284
1697
|
this.resetTerminalScreen();
|
|
2285
1698
|
this.ptyProcess?.clearBuffer?.();
|
|
2286
1699
|
this.onStatusChange?.();
|
|
2287
1700
|
}
|
|
2288
1701
|
|
|
2289
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
1702
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2290
1703
|
isReady(): boolean { return this.ready; }
|
|
2291
1704
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
1705
|
+
// ─── State machine property accessors (delegate to engine) ──────────────
|
|
1706
|
+
// These expose engine state for external callers (tests, debug tools, etc.)
|
|
1707
|
+
|
|
1708
|
+
get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
|
|
1709
|
+
set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
|
|
1710
|
+
|
|
1711
|
+
get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
|
|
1712
|
+
set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
|
|
1713
|
+
|
|
1714
|
+
get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
|
|
1715
|
+
set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
|
|
1716
|
+
|
|
1717
|
+
get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
|
|
1718
|
+
set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
|
|
1719
|
+
|
|
1720
|
+
get responseEpoch(): number { return this.engine.responseEpoch; }
|
|
1721
|
+
set responseEpoch(v: number) { this.engine.responseEpoch = v; }
|
|
1722
|
+
|
|
1723
|
+
get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
|
|
1724
|
+
set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
|
|
1725
|
+
|
|
1726
|
+
get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
|
|
1727
|
+
set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
|
|
1728
|
+
|
|
1729
|
+
get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
|
|
1730
|
+
set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
|
|
1731
|
+
|
|
1732
|
+
get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
|
|
1733
|
+
set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
|
|
1734
|
+
|
|
1735
|
+
get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
|
|
1736
|
+
set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
|
|
1737
|
+
|
|
1738
|
+
get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
|
|
1739
|
+
get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
|
|
1740
|
+
|
|
1741
|
+
get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
|
|
1742
|
+
get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
|
|
1743
|
+
|
|
1744
|
+
get finishRetryCount(): number { return this.engine.finishRetryCount; }
|
|
1745
|
+
set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
|
|
1746
|
+
|
|
1747
|
+
get traceSessionId(): string { return this.engine.getTraceSessionId(); }
|
|
1748
|
+
get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
|
|
1749
|
+
get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
|
|
1750
|
+
get traceSeq(): number { return this.engine.getTraceEntries().length; }
|
|
1751
|
+
|
|
1752
|
+
/** Expose engine's evaluateSettled for test access */
|
|
1753
|
+
evaluateSettled(): void {
|
|
1754
|
+
LOG.debug(
|
|
1755
|
+
'CLI',
|
|
1756
|
+
`[${this.cliType}] settled diagnostics delegated to state engine`);
|
|
1757
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1758
|
+
}
|
|
1759
|
+
/** Expose engine's scheduleSettle for test access */
|
|
1760
|
+
scheduleSettle(): void { this.engine.scheduleSettle(); }
|
|
1761
|
+
/** Expose engine's clearIdleFinishCandidate for test access */
|
|
1762
|
+
clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
|
|
1763
|
+
/** Expose engine's finishResponse for test access */
|
|
1764
|
+
finishResponse(): void { this.engine.finishResponse(); }
|
|
1765
|
+
/** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
|
|
1766
|
+
getSnapshot(): CliBufferSnapshot {
|
|
1767
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
1768
|
+
return {
|
|
1769
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
1770
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1771
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
1772
|
+
responseBuffer: this.responseBuffer,
|
|
1773
|
+
screenText,
|
|
1774
|
+
parseScreenText: this.getParseScreenText(screenText),
|
|
1775
|
+
workingDir: this.workingDir,
|
|
1776
|
+
providerSessionId: this.providerSessionId,
|
|
1777
|
+
runtimeSettings: this.runtimeSettings,
|
|
1778
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1779
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1780
|
+
lastOutputAt: this.lastOutputAt,
|
|
1781
|
+
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
1782
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1783
|
+
lastScreenSnapshot: this.lastScreenSnapshot,
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
isAlive(): boolean { return this.ptyProcess !== null; }
|
|
1787
|
+
flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
|
|
1788
|
+
|
|
1789
|
+
async writeRaw(data: string | Buffer): Promise<void> {
|
|
1790
|
+
const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
|
1791
|
+
await this.writeToPty(str);
|
|
2298
1792
|
}
|
|
2299
1793
|
|
|
2300
1794
|
resolveModal(buttonIndex: number): void {
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
this.activeModal = parsedModal;
|
|
2312
|
-
if (this.currentStatus !== 'waiting_approval') {
|
|
2313
|
-
this.setStatus('waiting_approval', 'resolve_modal_parse');
|
|
2314
|
-
this.onStatusChange?.();
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
} catch {
|
|
2318
|
-
// Ignore parse failures here; resolveModal falls back to current state.
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
if (!this.ptyProcess || ((this.currentStatus !== 'waiting_approval') && !modal)) return;
|
|
2322
|
-
this.clearIdleFinishCandidate('resolve_modal');
|
|
2323
|
-
this.recordTrace('resolve_modal', {
|
|
2324
|
-
buttonIndex,
|
|
2325
|
-
activeModal: modal,
|
|
2326
|
-
});
|
|
2327
|
-
this.activeModal = null;
|
|
2328
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
2329
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
2330
|
-
if (this.approvalExitTimeout) {
|
|
2331
|
-
clearTimeout(this.approvalExitTimeout);
|
|
2332
|
-
this.approvalExitTimeout = null;
|
|
2333
|
-
}
|
|
2334
|
-
this.setStatus('generating', 'approval_resolved');
|
|
2335
|
-
this.onStatusChange?.();
|
|
2336
|
-
if (buttonIndex in this.approvalKeys) {
|
|
2337
|
-
this.ptyProcess.write(this.approvalKeys[buttonIndex]);
|
|
2338
|
-
} else {
|
|
2339
|
-
const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
|
|
2340
|
-
const clampedIndex = buttonCount > 0
|
|
2341
|
-
? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
|
|
2342
|
-
: Math.max(0, buttonIndex);
|
|
2343
|
-
const DOWN = '\x1B[B';
|
|
2344
|
-
const keys = DOWN.repeat(clampedIndex) + '\r';
|
|
2345
|
-
this.ptyProcess.write(keys);
|
|
2346
|
-
}
|
|
1795
|
+
this.engine.resolveModal(buttonIndex);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
1799
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
1803
|
+
isApprovalRecentlyResolved(): boolean {
|
|
1804
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2347
1805
|
}
|
|
2348
1806
|
|
|
2349
1807
|
resize(cols: number, rows: number): void {
|
|
@@ -2357,7 +1815,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2357
1815
|
}
|
|
2358
1816
|
|
|
2359
1817
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2360
|
-
if (this.startupParseGate ||
|
|
1818
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2361
1819
|
try {
|
|
2362
1820
|
const parsed = this.getScriptParsedStatus();
|
|
2363
1821
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2369,18 +1827,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2369
1827
|
getDebugState(): Record<string, any> {
|
|
2370
1828
|
const screenText = sanitizeTerminalText(this.terminalScreen.getText());
|
|
2371
1829
|
const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
2372
|
-
const
|
|
2373
|
-
|
|
1830
|
+
const startupDetectedStatus = this.startupParseGate && !startupModal
|
|
1831
|
+
? this.runDetectStatus(this.recentOutputBuffer || screenText)
|
|
1832
|
+
: null;
|
|
1833
|
+
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2374
1834
|
const parsedDebugState = this.getParsedDebugState();
|
|
2375
1835
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
1836
|
+
const hasFinalAssistant = (p: any) => {
|
|
1837
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1838
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1839
|
+
};
|
|
1840
|
+
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1841
|
+
if (parsedDebugState?.status === 'error') {
|
|
1842
|
+
effectiveStatus = 'error';
|
|
1843
|
+
}
|
|
1844
|
+
// (fix) Mirror the getStatus contract: never surface waiting_approval
|
|
1845
|
+
// without a concrete modal. detectStatus alone can fire while
|
|
1846
|
+
// parseApproval is still null, and a bare debugState waiting_approval
|
|
1847
|
+
// confused dashboards reading the debug payload.
|
|
1848
|
+
const debugEffectiveModal = startupModal || this.engine.activeModal;
|
|
1849
|
+
if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
|
|
1850
|
+
effectiveStatus = 'waiting_approval';
|
|
1851
|
+
}
|
|
1852
|
+
if (
|
|
1853
|
+
effectiveStatus === 'idle'
|
|
1854
|
+
&& parsedDebugState?.status === 'generating'
|
|
1855
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
1856
|
+
) {
|
|
1857
|
+
effectiveStatus = 'generating';
|
|
1858
|
+
}
|
|
2376
1859
|
return {
|
|
2377
1860
|
type: this.cliType,
|
|
2378
1861
|
name: this.cliName,
|
|
2379
1862
|
providerResolution: this.providerResolutionMeta,
|
|
2380
1863
|
status: effectiveStatus,
|
|
2381
1864
|
projectedStatus: effectiveStatus,
|
|
2382
|
-
rawStatus: this.currentStatus,
|
|
2383
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1865
|
+
rawStatus: this.engine.currentStatus,
|
|
1866
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2384
1867
|
ready: effectiveReady,
|
|
2385
1868
|
startupParseGate: this.startupParseGate,
|
|
2386
1869
|
spawnAt: this.spawnAt,
|
|
@@ -2394,38 +1877,47 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2394
1877
|
providerSessionId: parsedDebugState.providerSessionId,
|
|
2395
1878
|
transcriptAuthority: parsedDebugState.transcriptAuthority,
|
|
2396
1879
|
coverage: parsedDebugState.coverage,
|
|
1880
|
+
errorMessage: parsedDebugState.errorMessage,
|
|
1881
|
+
errorReason: parsedDebugState.errorReason,
|
|
2397
1882
|
activeModal: parsedDebugState.activeModal,
|
|
2398
1883
|
messageCount: parsedMessages.length,
|
|
2399
1884
|
} : null,
|
|
2400
1885
|
screenText: screenText.slice(-4000),
|
|
2401
|
-
currentTurnScope: this.currentTurnScope,
|
|
1886
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2402
1887
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2403
1888
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2404
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2405
1889
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2406
1890
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2407
1891
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
2408
1892
|
sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
|
|
2409
1893
|
responseBuffer: this.responseBuffer.slice(-1000),
|
|
1894
|
+
pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
|
|
1895
|
+
id: message.id,
|
|
1896
|
+
role: message.role,
|
|
1897
|
+
content: message.content,
|
|
1898
|
+
queuedAt: message.queuedAt,
|
|
1899
|
+
source: message.source,
|
|
1900
|
+
})),
|
|
1901
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
2410
1902
|
lastOutputAt: this.lastOutputAt,
|
|
2411
1903
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2412
1904
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2413
1905
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2414
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2415
|
-
activeModal: startupModal || this.activeModal,
|
|
2416
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1906
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1907
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
1908
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2417
1909
|
sendDelayMs: this.sendDelayMs,
|
|
2418
1910
|
sendKey: this.sendKey,
|
|
2419
1911
|
submitStrategy: this.submitStrategy,
|
|
2420
1912
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2421
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2422
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1913
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1914
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2423
1915
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2424
1916
|
hasCliScripts: this.hasCliScripts(),
|
|
2425
|
-
scriptNames:
|
|
2426
|
-
traceSessionId: this.
|
|
2427
|
-
traceEntryCount: this.
|
|
2428
|
-
statusHistory: this.
|
|
1917
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1918
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1919
|
+
traceEntryCount: this.engine.getTraceEntries().length,
|
|
1920
|
+
statusHistory: this.engine.getStatusHistory().slice(-30),
|
|
2429
1921
|
timeouts: this.timeouts,
|
|
2430
1922
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2431
1923
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2435,20 +1927,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2435
1927
|
|
|
2436
1928
|
getTraceState(limit = 120): Record<string, any> {
|
|
2437
1929
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
1930
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2438
1931
|
return {
|
|
2439
|
-
sessionId: this.
|
|
1932
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2440
1933
|
providerResolution: this.providerResolutionMeta,
|
|
2441
|
-
entryCount:
|
|
2442
|
-
entries:
|
|
1934
|
+
entryCount: traceEntries.length,
|
|
1935
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2443
1936
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2444
1937
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2445
1938
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2446
1939
|
status: this.projectEffectiveStatus(),
|
|
2447
1940
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2448
|
-
rawStatus: this.currentStatus,
|
|
2449
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2450
|
-
activeModal: this.activeModal,
|
|
2451
|
-
currentTurnScope: this.currentTurnScope,
|
|
1941
|
+
rawStatus: this.engine.currentStatus,
|
|
1942
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1943
|
+
activeModal: this.engine.activeModal,
|
|
1944
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2452
1945
|
messages: [],
|
|
2453
1946
|
};
|
|
2454
1947
|
}
|