@adhdev/daemon-core 0.9.82-rc.15 → 0.9.82-rc.152
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/boot/process-hardening.d.ts +50 -0
- 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 +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +104 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +27 -0
- package/dist/commands/handler.d.ts +66 -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 +20 -6
- package/dist/index.js +13153 -3031
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13099 -3023
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- 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/cli-provider-instance.d.ts +6 -1
- package/dist/providers/contracts.d.ts +50 -5
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/index.d.ts +11 -0
- package/dist/providers/provider-loader.d.ts +19 -1
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -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 +6 -2
- package/src/boot/daemon-lifecycle.ts +17 -10
- package/src/boot/process-hardening.ts +89 -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 +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +664 -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 +57 -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 +1451 -52
- package/src/commands/cli-manager.ts +184 -3
- package/src/commands/handler.ts +547 -2
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3273 -428
- 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 +72 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- 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 +820 -61
- 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 +4 -11
- package/src/providers/cli-provider-instance.ts +380 -44
- package/src/providers/contracts.ts +52 -5
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/index.ts +23 -0
- package/src/providers/provider-loader.ts +294 -28
- package/src/providers/provider-schema.ts +31 -13
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +444 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +210 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- 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,48 @@ 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.
|
|
177
|
+
|
|
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; }
|
|
195
182
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Recent script invocations (oldest → newest). Exposed via the
|
|
185
|
+
* `get_chat_debug_bundle` daemon command so anyone debugging a
|
|
186
|
+
* stuck-status regression can read what each detectStatus /
|
|
187
|
+
* parseSession call actually saw and returned, instead of having
|
|
188
|
+
* to instrument the daemon.
|
|
189
|
+
*/
|
|
190
|
+
getScriptInvocationTrace() {
|
|
191
|
+
return this.runner.getInvocationTrace();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Returns the full raw PTY byte stream captured since adapter start.
|
|
196
|
+
* Used by the `record_provider_pty` IPC command to produce fixtures.
|
|
197
|
+
* Bounded by MAX_ACCUMULATED_BUFFER; older bytes may have been dropped.
|
|
198
|
+
*/
|
|
199
|
+
getAccumulatedRawBuffer(): { text: string; droppedChars: number } {
|
|
200
|
+
return {
|
|
201
|
+
text: this.accumulatedRawBuffer,
|
|
202
|
+
droppedChars: this.accumulatedRawBufferDroppedChars,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
|
|
200
206
|
private runtimeSettings: Record<string, any> = {};
|
|
201
207
|
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
202
208
|
private accumulatedBuffer: string = '';
|
|
@@ -216,10 +222,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
216
222
|
* Hermes turn (tool calls + reasoning + final bubble) without the
|
|
217
223
|
* rolling window pushing the turn's ╭─ opening line out of view. */
|
|
218
224
|
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
225
|
private parsedStatusCache: {
|
|
224
226
|
responseBuffer: string;
|
|
225
227
|
currentTurnScope: TurnParseScope | null;
|
|
@@ -233,11 +235,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
233
235
|
result: any;
|
|
234
236
|
} | null = null;
|
|
235
237
|
private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
236
|
-
private static readonly MAX_TRACE_ENTRIES = 250;
|
|
237
238
|
|
|
238
239
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
239
|
-
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
240
|
-
private static readonly MAX_FINISH_RETRIES = 2;
|
|
241
240
|
|
|
242
241
|
private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
|
|
243
242
|
const build = (droppedChars: number, maxChars: number) => droppedChars > 0
|
|
@@ -313,13 +312,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
313
312
|
if (
|
|
314
313
|
cached
|
|
315
314
|
&& cached.responseBuffer === this.responseBuffer
|
|
316
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
315
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
317
316
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
318
317
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
319
318
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
320
319
|
&& cached.screenText === this.lastScreenText
|
|
321
|
-
&& cached.currentStatus === this.currentStatus
|
|
322
|
-
&& cached.activeModal === this.activeModal
|
|
320
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
321
|
+
&& cached.activeModal === this.engine.activeModal
|
|
323
322
|
&& cached.cliName === this.cliName
|
|
324
323
|
) {
|
|
325
324
|
return cached.result;
|
|
@@ -343,86 +342,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
343
342
|
return this.timeouts.statusActivityHold;
|
|
344
343
|
}
|
|
345
344
|
|
|
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
345
|
// Resolved timeouts
|
|
427
346
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
428
347
|
|
|
@@ -432,7 +351,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
432
351
|
private readonly sendKey: string;
|
|
433
352
|
private readonly submitStrategy: 'wait_for_echo' | 'immediate';
|
|
434
353
|
private readonly requirePromptEchoBeforeSubmit: boolean;
|
|
435
|
-
private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
|
|
436
354
|
|
|
437
355
|
constructor(
|
|
438
356
|
provider: CliProviderModule,
|
|
@@ -441,6 +359,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
441
359
|
private extraEnv: Record<string, string> = {},
|
|
442
360
|
transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
|
|
443
361
|
) {
|
|
362
|
+
this.runner = new CliScriptRunner(provider.type);
|
|
444
363
|
this.provider = provider;
|
|
445
364
|
this.transportFactory = transportFactory;
|
|
446
365
|
this.cliType = provider.type;
|
|
@@ -458,10 +377,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
458
377
|
this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
|
|
459
378
|
this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
|
|
460
379
|
|
|
461
|
-
//
|
|
462
|
-
this.
|
|
463
|
-
|
|
464
|
-
|
|
380
|
+
// State machine engine — owns all status transitions
|
|
381
|
+
this.engine = new CliStateEngine(
|
|
382
|
+
provider,
|
|
383
|
+
this.runner,
|
|
384
|
+
this as unknown as CliTransportAccess,
|
|
385
|
+
{
|
|
386
|
+
onStatusChange: () => { this.onStatusChange?.(); },
|
|
387
|
+
onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
|
|
388
|
+
onTurnCompleted: () => { this.responseBuffer = ''; },
|
|
389
|
+
} satisfies CliStateEngineCallbacks,
|
|
390
|
+
resolvedConfig.timeouts,
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Scripts delegated to CliScriptRunner — adapter stays as transport.
|
|
394
|
+
// Pass the manifest tui block so the runner can build the
|
|
395
|
+
// declarativeDetectStatus / declarativeParseApproval SDK functions
|
|
396
|
+
// that v1 extended-tier overrides depend on (without these, scripts
|
|
397
|
+
// like codex-cli's detect_status v1 fail-closed with return 'idle'
|
|
398
|
+
// and the session sticks in `generating` forever).
|
|
399
|
+
this.runner.setScripts(provider.scripts || {}, provider.tui);
|
|
400
|
+
// Per-invocation wall-clock budget for provider scripts. Manifests may
|
|
401
|
+
// raise this for genuinely slow parsers, but the default (50ms) is the
|
|
402
|
+
// settle-loop safety net — any script that exceeds it gets flagged in
|
|
403
|
+
// the invocation trace and surfaced via the debug bundle so we can
|
|
404
|
+
// identify the offender instead of guessing why the settle loop hangs.
|
|
405
|
+
this.runner.setScriptCallBudget(provider.scriptCallBudgetMs ?? 50);
|
|
406
|
+
const scriptNames = this.runner.getScriptNames();
|
|
465
407
|
if (scriptNames.length > 0) {
|
|
466
408
|
LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
|
|
467
409
|
LOG.info(
|
|
@@ -487,14 +429,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
487
429
|
|
|
488
430
|
/** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
|
|
489
431
|
setCliScripts(scripts: CliScripts): void {
|
|
490
|
-
this.
|
|
432
|
+
this.runner.setScripts(scripts);
|
|
491
433
|
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(', ')}]`);
|
|
434
|
+
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
|
|
498
435
|
}
|
|
499
436
|
|
|
500
437
|
/** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
|
|
@@ -548,15 +485,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
548
485
|
});
|
|
549
486
|
|
|
550
487
|
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
488
|
|
|
561
489
|
try {
|
|
562
490
|
this.ptyProcess = this.transportFactory.spawn(
|
|
@@ -620,13 +548,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
620
548
|
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
621
549
|
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
|
|
622
550
|
this.flushPendingOutputParse();
|
|
623
|
-
this.recordTrace('exit', { exitCode });
|
|
624
551
|
this.ptyProcess = null;
|
|
625
|
-
this.
|
|
552
|
+
this.engine.onPtyExit();
|
|
626
553
|
this.ready = false;
|
|
627
554
|
this.startupParseGate = false;
|
|
628
555
|
this.spawnAt = 0;
|
|
629
|
-
this.
|
|
556
|
+
this.runner.resetSessionState();
|
|
630
557
|
this.onStatusChange?.();
|
|
631
558
|
});
|
|
632
559
|
|
|
@@ -637,15 +564,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
637
564
|
if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
|
|
638
565
|
this.resetTerminalScreen(24, 80);
|
|
639
566
|
this.pendingTerminalQueryTail = '';
|
|
640
|
-
this.currentTurnScope = null;
|
|
641
|
-
this.finishRetryCount = 0;
|
|
642
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
643
567
|
this.ready = false;
|
|
644
568
|
await this.ptyProcess.ready;
|
|
645
|
-
this.
|
|
646
|
-
runtimeMeta: this.getRuntimeMetadata(),
|
|
647
|
-
});
|
|
648
|
-
this.setStatus('starting', 'pty_ready');
|
|
569
|
+
this.engine.onSpawnReady();
|
|
649
570
|
this.scheduleStartupSettleCheck();
|
|
650
571
|
this.onStatusChange?.();
|
|
651
572
|
}
|
|
@@ -671,11 +592,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
671
592
|
if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
|
|
672
593
|
this.startupFirstOutputAt = now;
|
|
673
594
|
}
|
|
674
|
-
if (
|
|
675
|
-
this.clearIdleFinishCandidate('new_output');
|
|
595
|
+
if (rawData.length > 0 || cleanData.length > 0) {
|
|
596
|
+
this.engine.clearIdleFinishCandidate('new_output');
|
|
676
597
|
}
|
|
677
598
|
if (getDebugRuntimeConfig().collectDebugTrace) {
|
|
678
|
-
this.
|
|
599
|
+
this.engine.recordExternalTrace('output', {
|
|
679
600
|
rawLength: rawData.length,
|
|
680
601
|
cleanLength: cleanData.length,
|
|
681
602
|
rawPreview: summarizeCliTraceText(rawData, 300),
|
|
@@ -687,7 +608,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
687
608
|
this.scheduleStartupSettleCheck();
|
|
688
609
|
}
|
|
689
610
|
|
|
690
|
-
if (this.isWaitingForResponse && cleanData) {
|
|
611
|
+
if (this.engine.isWaitingForResponse && cleanData) {
|
|
691
612
|
const previousResponseLen = this.responseBuffer.length;
|
|
692
613
|
this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
|
|
693
614
|
this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
|
|
@@ -726,19 +647,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
726
647
|
// Keep turn-scope offsets aligned with the truncated buffer so scoped
|
|
727
648
|
// parses don't lose the beginning of a long turn (e.g. the Hermes
|
|
728
649
|
// ╭─ opening line) when the rolling window sheds bytes.
|
|
729
|
-
if (this.currentTurnScope) {
|
|
650
|
+
if (this.engine.currentTurnScope) {
|
|
730
651
|
if (droppedClean > 0) {
|
|
731
|
-
this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
|
|
652
|
+
this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
|
|
732
653
|
}
|
|
733
654
|
if (droppedRaw > 0) {
|
|
734
|
-
this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
|
|
655
|
+
this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
|
|
735
656
|
}
|
|
736
657
|
}
|
|
737
658
|
|
|
738
659
|
this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
|
|
739
660
|
|
|
740
661
|
// ─── Script-based status detection
|
|
741
|
-
this.scheduleSettle();
|
|
662
|
+
this.engine.scheduleSettle();
|
|
742
663
|
}
|
|
743
664
|
|
|
744
665
|
private resolveStartupState(
|
|
@@ -761,6 +682,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
761
682
|
if (stableMs < 2000) return;
|
|
762
683
|
|
|
763
684
|
const startupModal = this.runParseApproval(this.recentOutputBuffer);
|
|
685
|
+
const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
686
|
+
if (!startupModal && startupStatus !== 'idle') {
|
|
687
|
+
this.scheduleStartupSettleCheck();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
764
690
|
this.startupParseGate = false;
|
|
765
691
|
if (this.startupSettleTimer) {
|
|
766
692
|
clearTimeout(this.startupSettleTimer);
|
|
@@ -768,14 +694,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
768
694
|
}
|
|
769
695
|
this.ready = true;
|
|
770
696
|
if (startupModal) {
|
|
771
|
-
this.activeModal = startupModal;
|
|
772
|
-
this.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
697
|
+
this.engine.activeModal = startupModal;
|
|
698
|
+
this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
773
699
|
} else {
|
|
774
|
-
if (this.currentStatus === 'waiting_approval' || this.activeModal) {
|
|
775
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
700
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
|
|
701
|
+
this.engine.lastApprovalResolvedAt = Date.now();
|
|
776
702
|
}
|
|
777
|
-
this.activeModal = null;
|
|
778
|
-
|
|
703
|
+
this.engine.activeModal = null;
|
|
704
|
+
// Clear the in-flight turn flag at the same time we declare
|
|
705
|
+
// startup-idle. Otherwise the next settled evaluation sees
|
|
706
|
+
// isWaitingForResponse=true + recent CLI welcome-screen paints
|
|
707
|
+
// and flips us right back to generating via the hold path
|
|
708
|
+
// (the "startup → generating → idle → generating → idle"
|
|
709
|
+
// flicker the user observed on claude-cli launch).
|
|
710
|
+
this.engine.isWaitingForResponse = false;
|
|
711
|
+
this.engine.currentTurnScope = null;
|
|
712
|
+
this.engine.setStatus('idle', `startup_ready:${trigger}`);
|
|
779
713
|
}
|
|
780
714
|
LOG.info(
|
|
781
715
|
'CLI',
|
|
@@ -801,88 +735,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
801
735
|
}, delayMs);
|
|
802
736
|
}
|
|
803
737
|
|
|
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
738
|
private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
|
|
887
739
|
const startedAt = Date.now();
|
|
888
740
|
let loggedWait = false;
|
|
@@ -892,7 +744,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
892
744
|
const screenText = this.terminalScreen.getText() || '';
|
|
893
745
|
const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
|
|
894
746
|
const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
895
|
-
const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
747
|
+
const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
|
|
896
748
|
const interactiveReady = status === 'idle'
|
|
897
749
|
&& stableMs >= 700
|
|
898
750
|
&& recentlyOutput >= 350;
|
|
@@ -926,637 +778,84 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
926
778
|
|
|
927
779
|
private clearAllTimers(): void {
|
|
928
780
|
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
781
|
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
782
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
936
783
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
784
|
+
this.engine.clearAllTimers();
|
|
937
785
|
}
|
|
938
786
|
|
|
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;
|
|
999
|
-
|
|
1000
|
-
if (!this.isWaitingForResponse && !this.currentTurnScope && !this.activeModal && !this.parseErrorMessage) {
|
|
1001
|
-
const tail = this.settledBuffer || this.recentOutputBuffer;
|
|
1002
|
-
const modal = this.runParseApproval(tail);
|
|
1003
|
-
const lightweightStatus = this.cliScripts?.detectStatus
|
|
1004
|
-
? this.runDetectStatus(tail)
|
|
1005
|
-
: null;
|
|
1006
|
-
if (!modal && lightweightStatus === 'idle' && this.currentStatus === 'idle') {
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
const session = this.runParseSession();
|
|
1012
|
-
if (!session) return;
|
|
1013
|
-
|
|
1014
|
-
const { status, messages, parsedStatus } = session;
|
|
1015
|
-
const modal = (session as any).activeModal ?? session.modal ?? null;
|
|
1016
|
-
const parsedMessages = normalizeCliParsedMessages(messages, {
|
|
1017
|
-
scope: null,
|
|
1018
|
-
lastOutputAt: this.lastOutputAt,
|
|
1019
|
-
});
|
|
787
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
1020
788
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
responseBuffer: this.responseBuffer,
|
|
1040
|
-
partialResponse: this.responseBuffer,
|
|
1041
|
-
scope: this.currentTurnScope,
|
|
1042
|
-
}),
|
|
789
|
+
runParseSession(): ParsedSession | null {
|
|
790
|
+
const screenText = this.terminalScreen.getText();
|
|
791
|
+
const parseScreenText = this.getParseScreenText(screenText);
|
|
792
|
+
const tail = this.recentOutputBuffer.slice(-500);
|
|
793
|
+
const input = buildCliParseInput({
|
|
794
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
795
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
796
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
797
|
+
terminalScreenText: parseScreenText,
|
|
798
|
+
workingDir: this.workingDir,
|
|
799
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
800
|
+
historySessionId: this.providerSessionId || undefined,
|
|
801
|
+
baseMessages: [],
|
|
802
|
+
partialResponse: this.responseBuffer,
|
|
803
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
804
|
+
scope: this.engine.currentTurnScope,
|
|
805
|
+
runtimeSettings: this.runtimeSettings,
|
|
806
|
+
spawnAt: this.spawnAt,
|
|
1043
807
|
});
|
|
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
|
-
}),
|
|
808
|
+
const session = this.runner.parseSession({
|
|
809
|
+
...input,
|
|
810
|
+
tail,
|
|
811
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1168
812
|
});
|
|
1169
|
-
this.
|
|
813
|
+
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
814
|
+
return session;
|
|
1170
815
|
}
|
|
1171
816
|
|
|
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
|
-
}),
|
|
817
|
+
runDetectStatus(text: string): string | null {
|
|
818
|
+
const screenText = this.terminalScreen.getText();
|
|
819
|
+
const tail = text.slice(-500);
|
|
820
|
+
return this.runner.detectStatus({
|
|
821
|
+
tail,
|
|
822
|
+
screenText,
|
|
823
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
824
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
825
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
826
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1339
827
|
});
|
|
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
828
|
}
|
|
1376
829
|
|
|
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),
|
|
830
|
+
runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
831
|
+
const screenText = this.terminalScreen.getText();
|
|
832
|
+
const buffer = screenText || this.accumulatedBuffer;
|
|
833
|
+
return this.runner.parseApproval({
|
|
834
|
+
buffer,
|
|
835
|
+
screenText,
|
|
836
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
837
|
+
tail,
|
|
838
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
839
|
+
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
840
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1408
841
|
});
|
|
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
|
-
}
|
|
1511
|
-
|
|
1512
|
-
private runDetectStatus(text: string): string | null {
|
|
1513
|
-
if (!this.cliScripts?.detectStatus) return null;
|
|
1514
|
-
try {
|
|
1515
|
-
const screenText = this.terminalScreen.getText();
|
|
1516
|
-
const status = this.invokeCliScript<string | null>(this.cliScripts.detectStatus, {
|
|
1517
|
-
tail: text.slice(-500),
|
|
1518
|
-
screenText,
|
|
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
842
|
}
|
|
1530
843
|
|
|
1531
|
-
private
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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;
|
|
844
|
+
private applyParsedSessionMetadata(parsed: any): void {
|
|
845
|
+
const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
|
|
846
|
+
? parsed.providerSessionId.trim()
|
|
847
|
+
: '';
|
|
848
|
+
if (providerSessionId && providerSessionId !== this.providerSessionId) {
|
|
849
|
+
this.providerSessionId = providerSessionId;
|
|
850
|
+
this.updateRuntimeMeta({ providerSessionId });
|
|
1548
851
|
}
|
|
1549
852
|
}
|
|
1550
853
|
|
|
1551
|
-
private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
|
|
1552
|
-
return !!(startupModal || this.activeModal);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
854
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1556
855
|
if (this.parseErrorMessage) return 'error';
|
|
1557
|
-
if (this.
|
|
1558
|
-
if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
|
|
1559
|
-
return this.currentStatus;
|
|
856
|
+
if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
|
|
857
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
|
|
858
|
+
return this.engine.currentStatus;
|
|
1560
859
|
}
|
|
1561
860
|
|
|
1562
861
|
// ─── Public API (CliAdapter) ───────────────────
|
|
@@ -1564,17 +863,73 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1564
863
|
getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
|
|
1565
864
|
const allowParse = options.allowParse !== false;
|
|
1566
865
|
const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
866
|
+
const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
|
|
867
|
+
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
868
|
+
: null;
|
|
1567
869
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1568
|
-
let effectiveModal = startupModal || this.activeModal;
|
|
870
|
+
let effectiveModal = startupModal || this.engine.activeModal;
|
|
871
|
+
// (fix) When we have no captured modal yet, take one more live attempt
|
|
872
|
+
// with the current screen text — the engine's settle pass can miss
|
|
873
|
+
// the modal when it happens to fire exactly between writes, and
|
|
874
|
+
// without a modal here the dashboard could never show the buttons.
|
|
875
|
+
// We deliberately do NOT overwrite an existing engine.activeModal so
|
|
876
|
+
// a stable matched modal wins. This runs even outside the startup
|
|
877
|
+
// gate because Claude's approval frames can appear long after launch.
|
|
878
|
+
if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
|
|
879
|
+
const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
|
|
880
|
+
if (liveDetect === 'waiting_approval') {
|
|
881
|
+
const liveModal = this.runParseApproval(this.terminalScreen.getText())
|
|
882
|
+
|| this.runParseApproval(this.recentOutputBuffer);
|
|
883
|
+
// (fix) Only surface modals that have at least one non-empty
|
|
884
|
+
// button. Half-rendered approval frames produced
|
|
885
|
+
// { message, buttons: [] } briefly — auto-approve would then
|
|
886
|
+
// pick buttonIndex=-1 and resolveModal would write the
|
|
887
|
+
// provider's index-0 key into the prompt. Skip those frames
|
|
888
|
+
// and let a later evaluate find the complete modal.
|
|
889
|
+
const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
|
|
890
|
+
&& liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
|
|
891
|
+
if (liveModal && buttonsOk) {
|
|
892
|
+
effectiveModal = liveModal;
|
|
893
|
+
if (!this.engine.activeModal) this.engine.activeModal = liveModal;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Only surface waiting_approval when we ALSO have a concrete modal
|
|
898
|
+
// (message + buttons). detectStatus alone can fire while parseApproval
|
|
899
|
+
// is still null — the engine logs "detectStatus=waiting_approval but
|
|
900
|
+
// parseApproval returned null; ignoring". Without this guard getStatus
|
|
901
|
+
// was shipping a bare waiting_approval with activeModal=null and the
|
|
902
|
+
// user perceived the flow as broken.
|
|
903
|
+
if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
|
|
904
|
+
effectiveStatus = 'waiting_approval';
|
|
905
|
+
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
906
|
+
effectiveStatus = 'idle';
|
|
907
|
+
}
|
|
1569
908
|
if (allowParse && !startupModal && !effectiveModal) {
|
|
1570
909
|
const parsed = this.getFreshParsedStatusCache();
|
|
1571
910
|
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
1572
911
|
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
1573
912
|
? parsed.activeModal
|
|
1574
913
|
: null;
|
|
914
|
+
const hasFinalAssistant = (p: any) => {
|
|
915
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
916
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
917
|
+
};
|
|
1575
918
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1576
919
|
effectiveStatus = 'waiting_approval';
|
|
1577
920
|
effectiveModal = parsedModal;
|
|
921
|
+
} else if (
|
|
922
|
+
effectiveStatus === 'idle'
|
|
923
|
+
&& parsed?.status === 'generating'
|
|
924
|
+
&& !hasFinalAssistant(parsed)
|
|
925
|
+
) {
|
|
926
|
+
effectiveStatus = 'generating';
|
|
927
|
+
} else if (
|
|
928
|
+
effectiveStatus === 'generating'
|
|
929
|
+
&& parsed?.status === 'idle'
|
|
930
|
+
&& hasFinalAssistant(parsed)
|
|
931
|
+
) {
|
|
932
|
+
effectiveStatus = 'idle';
|
|
1578
933
|
}
|
|
1579
934
|
}
|
|
1580
935
|
const bufferState = this.getBufferState();
|
|
@@ -1583,8 +938,17 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1583
938
|
messages: [],
|
|
1584
939
|
workingDir: this.workingDir,
|
|
1585
940
|
activeModal: effectiveModal,
|
|
1586
|
-
|
|
1587
|
-
|
|
941
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
942
|
+
pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
|
|
943
|
+
id: message.id,
|
|
944
|
+
role: message.role,
|
|
945
|
+
content: message.content,
|
|
946
|
+
queuedAt: message.queuedAt,
|
|
947
|
+
source: message.source,
|
|
948
|
+
})),
|
|
949
|
+
errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
|
|
950
|
+
errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
|
|
951
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1588
952
|
...(bufferState ? { bufferState } : {}),
|
|
1589
953
|
};
|
|
1590
954
|
}
|
|
@@ -1600,15 +964,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1600
964
|
const cached = this.parsedStatusCache;
|
|
1601
965
|
const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
|
|
1602
966
|
if (
|
|
1603
|
-
|
|
967
|
+
!this.providerOwnsTranscript()
|
|
968
|
+
&& cached
|
|
1604
969
|
&& cached.responseBuffer === this.responseBuffer
|
|
1605
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
970
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1606
971
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1607
972
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1608
973
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1609
974
|
&& cached.screenText === parseScreenText
|
|
1610
|
-
&& cached.currentStatus === this.currentStatus
|
|
1611
|
-
&& cached.activeModal === this.activeModal
|
|
975
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
976
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1612
977
|
&& cached.cliName === this.cliName
|
|
1613
978
|
) {
|
|
1614
979
|
return cached.result;
|
|
@@ -1623,14 +988,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1623
988
|
const bufferState = this.getBufferState();
|
|
1624
989
|
const result = {
|
|
1625
990
|
id: (parsed as any).id || 'cli_session',
|
|
1626
|
-
status: parsed.status || this.currentStatus,
|
|
991
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1627
992
|
title: (parsed as any).title || this.cliName,
|
|
1628
993
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1629
994
|
scope: null,
|
|
1630
995
|
lastOutputAt: this.lastOutputAt,
|
|
1631
996
|
}),
|
|
1632
997
|
activeModal,
|
|
1633
|
-
providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
|
|
998
|
+
providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
|
|
999
|
+
errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
|
|
1000
|
+
? (parsed as any).errorMessage.trim()
|
|
1001
|
+
: undefined,
|
|
1002
|
+
errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
|
|
1003
|
+
? (parsed as any).errorReason.trim()
|
|
1004
|
+
: undefined,
|
|
1634
1005
|
...(bufferState ? { bufferState } : {}),
|
|
1635
1006
|
...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
|
|
1636
1007
|
? { transcriptAuthority: (parsed as any).transcriptAuthority }
|
|
@@ -1642,13 +1013,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1642
1013
|
|
|
1643
1014
|
this.parsedStatusCache = {
|
|
1644
1015
|
responseBuffer: this.responseBuffer,
|
|
1645
|
-
currentTurnScope: this.currentTurnScope,
|
|
1016
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1646
1017
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1647
1018
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1648
1019
|
accumulatedRawBufferKey,
|
|
1649
1020
|
screenText: parseScreenText,
|
|
1650
|
-
currentStatus: this.currentStatus,
|
|
1651
|
-
activeModal: this.activeModal,
|
|
1021
|
+
currentStatus: this.engine.currentStatus,
|
|
1022
|
+
activeModal: this.engine.activeModal,
|
|
1652
1023
|
cliName: this.cliName,
|
|
1653
1024
|
result,
|
|
1654
1025
|
};
|
|
@@ -1656,22 +1027,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1656
1027
|
}
|
|
1657
1028
|
|
|
1658
1029
|
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
1030
|
const input = buildCliParseInput({
|
|
1664
1031
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1665
1032
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1666
1033
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1667
1034
|
terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
|
|
1035
|
+
workingDir: this.workingDir,
|
|
1036
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1037
|
+
historySessionId: this.providerSessionId || undefined,
|
|
1668
1038
|
baseMessages: [],
|
|
1669
1039
|
partialResponse: this.responseBuffer,
|
|
1670
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1671
|
-
scope: this.currentTurnScope,
|
|
1040
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1041
|
+
scope: this.engine.currentTurnScope,
|
|
1672
1042
|
runtimeSettings: this.runtimeSettings,
|
|
1043
|
+
spawnAt: this.spawnAt,
|
|
1673
1044
|
});
|
|
1674
|
-
return await Promise.resolve(this.
|
|
1045
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1675
1046
|
...input,
|
|
1676
1047
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1677
1048
|
}));
|
|
@@ -1683,7 +1054,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1683
1054
|
|
|
1684
1055
|
/** Whether this adapter has CLI scripts loaded */
|
|
1685
1056
|
hasCliScripts(): boolean {
|
|
1686
|
-
return
|
|
1057
|
+
return this.runner.hasDetectStatus();
|
|
1687
1058
|
}
|
|
1688
1059
|
|
|
1689
1060
|
/**
|
|
@@ -1692,24 +1063,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1692
1063
|
*/
|
|
1693
1064
|
async resolveAction(data: any): Promise<void> {
|
|
1694
1065
|
let promptText = '';
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1066
|
+
try {
|
|
1067
|
+
promptText = this.runner.invokeByName('resolveAction', data);
|
|
1068
|
+
} catch {
|
|
1069
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
|
|
1070
|
+
return;
|
|
1701
1071
|
}
|
|
1702
1072
|
if (!promptText) {
|
|
1703
|
-
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not
|
|
1073
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
|
|
1704
1074
|
return;
|
|
1705
1075
|
}
|
|
1706
1076
|
await this.sendMessage(promptText);
|
|
1707
1077
|
}
|
|
1708
1078
|
|
|
1709
1079
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
1710
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
1711
|
-
if (this.hasActionableApproval()) return false;
|
|
1712
|
-
|
|
1080
|
+
if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
|
|
1081
|
+
if (this.engine.hasActionableApproval()) return false;
|
|
1082
|
+
// If there's already meaningful response content beyond the echoed prompt, not stuck
|
|
1083
|
+
if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
|
|
1713
1084
|
const screenText = this.terminalScreen.getText();
|
|
1714
1085
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
1715
1086
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -1718,20 +1089,41 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1718
1089
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
1719
1090
|
}
|
|
1720
1091
|
|
|
1092
|
+
private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
|
|
1093
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
1094
|
+
if (!raw) return false;
|
|
1095
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1096
|
+
if (!normalizedPrompt) return true;
|
|
1097
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
1098
|
+
if (!normalizedBuffer) return false;
|
|
1099
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1100
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1101
|
+
const remainder = normalizedBuffer
|
|
1102
|
+
.slice(normalizedPrompt.length)
|
|
1103
|
+
.replace(/[─═\-]+/g, '')
|
|
1104
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1105
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1106
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1107
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1108
|
+
.replace(/esctointerrupt/gi, '')
|
|
1109
|
+
.replace(/❯/g, '')
|
|
1110
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1111
|
+
.trim();
|
|
1112
|
+
return remainder.length > 0;
|
|
1113
|
+
}
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1721
1117
|
private async writeToPty(data: string): Promise<void> {
|
|
1722
1118
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1723
1119
|
await this.ptyProcess.write(data);
|
|
1724
1120
|
}
|
|
1725
1121
|
|
|
1726
1122
|
private resetPendingSendState(reason: string): void {
|
|
1727
|
-
this.isWaitingForResponse = false;
|
|
1728
1123
|
this.responseBuffer = '';
|
|
1729
|
-
this.currentTurnScope = null;
|
|
1730
|
-
this.submitPendingUntil = 0;
|
|
1731
|
-
this.clearIdleFinishCandidate(reason);
|
|
1732
1124
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1733
|
-
|
|
1734
|
-
|
|
1125
|
+
this.engine.resetActiveTurnState();
|
|
1126
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
1735
1127
|
}
|
|
1736
1128
|
|
|
1737
1129
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
@@ -1748,42 +1140,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1748
1140
|
}
|
|
1749
1141
|
this.responseTimeout = setTimeout(() => {
|
|
1750
1142
|
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
|
-
});
|
|
1143
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
1768
1144
|
|
|
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
|
|
1145
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
1773
1146
|
// normal settled parser instead and keep the turn open unless the provider
|
|
1774
1147
|
// 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
|
-
});
|
|
1148
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1149
|
+
|
|
1150
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
1787
1151
|
this.armResponseTimeout();
|
|
1788
1152
|
}
|
|
1789
1153
|
}, timeoutMs);
|
|
@@ -1798,34 +1162,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1798
1162
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
1799
1163
|
this.submitRetryTimer = null;
|
|
1800
1164
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1801
|
-
|
|
1802
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1165
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1803
1166
|
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
1167
|
this.writeSubmitKeyForRetry('submit_retry');
|
|
1811
|
-
if (attempt >= 3) { this.submitRetryUsed = true; return; }
|
|
1168
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
1812
1169
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
1813
1170
|
}
|
|
1814
1171
|
|
|
1815
1172
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
1816
1173
|
this.submitRetryTimer = null;
|
|
1817
1174
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1818
|
-
|
|
1819
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1175
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1820
1176
|
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
1177
|
this.writeSubmitKeyForRetry('immediate_retry');
|
|
1828
|
-
this.submitRetryUsed = true;
|
|
1178
|
+
this.engine.submitRetryUsed = true;
|
|
1829
1179
|
}
|
|
1830
1180
|
|
|
1831
1181
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -1833,13 +1183,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1833
1183
|
completion.resolveOnce();
|
|
1834
1184
|
return;
|
|
1835
1185
|
}
|
|
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
|
-
});
|
|
1186
|
+
this.engine.submitPendingUntil = 0;
|
|
1843
1187
|
void this.writeToPty(this.sendKey).then(() => {
|
|
1844
1188
|
this.commitSendUserTurn(state);
|
|
1845
1189
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -1849,13 +1193,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1849
1193
|
}
|
|
1850
1194
|
|
|
1851
1195
|
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
|
-
});
|
|
1196
|
+
this.engine.submitPendingUntil = 0;
|
|
1859
1197
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
1860
1198
|
this.commitSendUserTurn(state);
|
|
1861
1199
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -1899,7 +1237,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1899
1237
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
1900
1238
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
1901
1239
|
};
|
|
1902
|
-
|
|
1240
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1241
|
+
|
|
1903
1242
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
1904
1243
|
// At this point the prompt text write already completed. Rejecting without
|
|
1905
1244
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -1924,12 +1263,110 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1924
1263
|
), 50);
|
|
1925
1264
|
}
|
|
1926
1265
|
|
|
1927
|
-
async sendMessage(text: string): Promise<void> {
|
|
1266
|
+
async sendMessage(text: string, options: { force?: boolean } = {}): Promise<void> {
|
|
1267
|
+
if (options.force === true) {
|
|
1268
|
+
await this.forceSendMessage(text);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
await this.sendMessageNow(text, true);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async forceSendMessage(text: string): Promise<void> {
|
|
1275
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1276
|
+
const content = String(text || '');
|
|
1277
|
+
if (!content.trim()) return;
|
|
1278
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
|
|
1279
|
+
await this.writeToPty(content + this.sendKey);
|
|
1280
|
+
this.onStatusChange?.();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private enqueuePendingOutboundMessage(text: string, reason: string): void {
|
|
1284
|
+
const content = String(text || '');
|
|
1285
|
+
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
1286
|
+
if (duplicate) {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const queuedAt = Date.now();
|
|
1290
|
+
const message: PendingOutboundMessage = {
|
|
1291
|
+
id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
|
|
1292
|
+
role: 'user',
|
|
1293
|
+
content,
|
|
1294
|
+
queuedAt,
|
|
1295
|
+
source: 'sendMessage',
|
|
1296
|
+
};
|
|
1297
|
+
this.pendingOutboundQueue.push(message);
|
|
1298
|
+
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
1299
|
+
this.onStatusChange?.();
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
1303
|
+
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
1304
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
1305
|
+
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1306
|
+
? String(parsedStatusBeforeSend.status)
|
|
1307
|
+
: '';
|
|
1308
|
+
const hasFinalAssistant = (p: any) => {
|
|
1309
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1310
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1311
|
+
};
|
|
1312
|
+
if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
|
|
1313
|
+
if (this.engine.currentStatus === 'generating') return 'current_status_generating';
|
|
1314
|
+
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
|
|
1315
|
+
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1316
|
+
const parsedHasActionableModal = Boolean(
|
|
1317
|
+
parsedModal
|
|
1318
|
+
&& Array.isArray(parsedModal.buttons)
|
|
1319
|
+
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1320
|
+
);
|
|
1321
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1322
|
+
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1323
|
+
&& !this.engine.isWaitingForResponse
|
|
1324
|
+
&& !this.engine.currentTurnScope
|
|
1325
|
+
&& !this.engine.hasActionableApproval()
|
|
1326
|
+
&& !parsedHasActionableModal;
|
|
1327
|
+
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
1328
|
+
}
|
|
1329
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
private schedulePendingOutboundFlush(delayMs = 0): void {
|
|
1334
|
+
if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
|
|
1335
|
+
this.pendingOutboundFlushTimer = setTimeout(() => {
|
|
1336
|
+
this.pendingOutboundFlushTimer = null;
|
|
1337
|
+
void this.flushPendingOutboundQueue();
|
|
1338
|
+
}, Math.max(0, delayMs));
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
private async flushPendingOutboundQueue(): Promise<void> {
|
|
1342
|
+
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
1343
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
1344
|
+
this.pendingOutboundFlushInFlight = true;
|
|
1345
|
+
try {
|
|
1346
|
+
while (this.pendingOutboundQueue.length > 0) {
|
|
1347
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
1348
|
+
const next = this.pendingOutboundQueue[0];
|
|
1349
|
+
try {
|
|
1350
|
+
await this.sendMessageNow(next.content, false);
|
|
1351
|
+
this.pendingOutboundQueue.shift();
|
|
1352
|
+
this.onStatusChange?.();
|
|
1353
|
+
} catch (error: any) {
|
|
1354
|
+
LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
|
|
1355
|
+
this.schedulePendingOutboundFlush(1000);
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
} finally {
|
|
1360
|
+
this.pendingOutboundFlushInFlight = false;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
|
|
1928
1365
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1929
1366
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
1930
1367
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
1931
|
-
&& this.isWaitingForResponse
|
|
1932
|
-
&& !this.hasActionableApproval();
|
|
1368
|
+
&& this.engine.isWaitingForResponse
|
|
1369
|
+
&& !this.engine.hasActionableApproval();
|
|
1933
1370
|
if (this.startupParseGate) {
|
|
1934
1371
|
const deadline = Date.now() + 10000;
|
|
1935
1372
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -1937,27 +1374,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1937
1374
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1938
1375
|
}
|
|
1939
1376
|
}
|
|
1377
|
+
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
1378
|
+
? (() => {
|
|
1379
|
+
try {
|
|
1380
|
+
return this.getScriptParsedStatus?.() || null;
|
|
1381
|
+
} catch {
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
})()
|
|
1385
|
+
: null;
|
|
1386
|
+
const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
|
|
1387
|
+
if (allowQueue && queueReason) {
|
|
1388
|
+
this.enqueuePendingOutboundMessage(text, queueReason);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1940
1391
|
if (!allowInterventionPrompt) {
|
|
1941
1392
|
await this.waitForInteractivePrompt();
|
|
1942
1393
|
}
|
|
1943
1394
|
if (!this.ready) {
|
|
1944
1395
|
this.resolveStartupState('send_precheck');
|
|
1945
|
-
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1396
|
+
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
1946
1397
|
this.ready = true;
|
|
1947
1398
|
this.startupParseGate = false;
|
|
1399
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1948
1400
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
1949
1401
|
}
|
|
1950
1402
|
}
|
|
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;
|
|
1403
|
+
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
1961
1404
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1962
1405
|
? String(parsedStatusBeforeSend.status)
|
|
1963
1406
|
: '';
|
|
@@ -1968,45 +1411,48 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1968
1411
|
&& Array.isArray(parsedModal.buttons)
|
|
1969
1412
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1970
1413
|
);
|
|
1971
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1414
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1972
1415
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1973
|
-
&& !this.isWaitingForResponse
|
|
1974
|
-
&& !this.currentTurnScope
|
|
1975
|
-
&& !this.hasActionableApproval()
|
|
1416
|
+
&& !this.engine.isWaitingForResponse
|
|
1417
|
+
&& !this.engine.currentTurnScope
|
|
1418
|
+
&& !this.engine.hasActionableApproval()
|
|
1976
1419
|
&& !parsedHasActionableModal;
|
|
1977
1420
|
if (!terminalLooksIdle) {
|
|
1421
|
+
if (allowQueue) {
|
|
1422
|
+
this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1978
1425
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1979
1426
|
}
|
|
1980
1427
|
}
|
|
1981
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1982
|
-
|
|
1428
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1429
|
+
const snap = this.getSnapshot();
|
|
1430
|
+
if (
|
|
1431
|
+
!this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
|
|
1432
|
+
&& !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
|
|
1433
|
+
) {
|
|
1434
|
+
if (allowQueue) {
|
|
1435
|
+
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1983
1438
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1984
1439
|
}
|
|
1985
1440
|
}
|
|
1986
|
-
this.isWaitingForResponse = true;
|
|
1987
1441
|
this.responseBuffer = '';
|
|
1988
|
-
|
|
1989
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1990
|
-
this.clearIdleFinishCandidate('send_message');
|
|
1991
|
-
this.currentTurnScope = {
|
|
1442
|
+
const turnScope: TurnParseScope = {
|
|
1992
1443
|
prompt: text,
|
|
1993
1444
|
startedAt: Date.now(),
|
|
1994
1445
|
bufferStart: this.accumulatedBuffer.length,
|
|
1995
1446
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
1996
1447
|
};
|
|
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);
|
|
1448
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2006
1449
|
if (this.submitRetryTimer) {
|
|
2007
1450
|
clearTimeout(this.submitRetryTimer);
|
|
2008
1451
|
this.submitRetryTimer = null;
|
|
2009
1452
|
}
|
|
1453
|
+
this.engine.onTurnStarted(turnScope);
|
|
1454
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1455
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2010
1456
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2011
1457
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2012
1458
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2019,12 +1465,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2019
1465
|
retryDelayMs,
|
|
2020
1466
|
didCommitUserTurn: false,
|
|
2021
1467
|
};
|
|
2022
|
-
|
|
2023
|
-
clearTimeout(this.settleTimer);
|
|
2024
|
-
this.settleTimer = null;
|
|
2025
|
-
}
|
|
2026
|
-
this.responseEpoch += 1;
|
|
2027
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1468
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2028
1469
|
await new Promise<void>((resolve, reject) => {
|
|
2029
1470
|
let resolved = false;
|
|
2030
1471
|
const completion: SendMessageCompletion = {
|
|
@@ -2047,24 +1488,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2047
1488
|
}
|
|
2048
1489
|
|
|
2049
1490
|
if (submitDelayMs > 0) {
|
|
2050
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1491
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2051
1492
|
}
|
|
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
1493
|
const submitStartedAt = Date.now();
|
|
2059
1494
|
void this.writeToPty(text).then(
|
|
2060
1495
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2061
1496
|
completion.rejectOnce,
|
|
2062
1497
|
);
|
|
2063
1498
|
});
|
|
1499
|
+
// Schedule settle after successful send
|
|
1500
|
+
this.engine.scheduleSettle();
|
|
2064
1501
|
}
|
|
2065
1502
|
|
|
2066
1503
|
getPartialResponse(): string {
|
|
2067
|
-
if (!this.isWaitingForResponse) return '';
|
|
1504
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2068
1505
|
return this.responseBuffer;
|
|
2069
1506
|
}
|
|
2070
1507
|
|
|
@@ -2077,10 +1514,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2077
1514
|
cliType: this.cliType,
|
|
2078
1515
|
cliName: this.cliName,
|
|
2079
1516
|
workingDir: this.workingDir,
|
|
2080
|
-
currentStatus: this.currentStatus,
|
|
1517
|
+
currentStatus: this.engine.currentStatus,
|
|
2081
1518
|
ready: this.ready,
|
|
2082
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2083
|
-
activeModal: this.activeModal,
|
|
1519
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1520
|
+
activeModal: this.engine.activeModal,
|
|
2084
1521
|
parseErrorMessage: this.parseErrorMessage,
|
|
2085
1522
|
messageCounts: {
|
|
2086
1523
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2106,10 +1543,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2106
1543
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2107
1544
|
},
|
|
2108
1545
|
parser: {
|
|
2109
|
-
scriptNames:
|
|
2110
|
-
traceSessionId: this.
|
|
2111
|
-
traceSeq: this.
|
|
2112
|
-
currentTurnScope: this.currentTurnScope,
|
|
1546
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1547
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1548
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1549
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2113
1550
|
parsedStatusCache: parsedResult
|
|
2114
1551
|
? {
|
|
2115
1552
|
id: parsedResult.id,
|
|
@@ -2122,26 +1559,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2122
1559
|
activeModal: parsedResult.activeModal,
|
|
2123
1560
|
}
|
|
2124
1561
|
: null,
|
|
2125
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2126
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1562
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1563
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2127
1564
|
},
|
|
2128
1565
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2129
|
-
statusHistory: this.
|
|
2130
|
-
traceEntries: this.
|
|
1566
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1567
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2131
1568
|
timing: {
|
|
2132
1569
|
spawnAt: this.spawnAt,
|
|
2133
1570
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2134
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2135
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2136
|
-
responseEpoch: this.responseEpoch,
|
|
1571
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1572
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1573
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2137
1574
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2138
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1575
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2139
1576
|
},
|
|
2140
1577
|
finish: {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1578
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1579
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1580
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2145
1581
|
},
|
|
2146
1582
|
};
|
|
2147
1583
|
}
|
|
@@ -2152,6 +1588,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2152
1588
|
}
|
|
2153
1589
|
|
|
2154
1590
|
updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
|
|
1591
|
+
const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
|
|
1592
|
+
? meta.providerSessionId.trim()
|
|
1593
|
+
: '';
|
|
1594
|
+
if (nextProviderSessionId) {
|
|
1595
|
+
this.providerSessionId = nextProviderSessionId;
|
|
1596
|
+
}
|
|
2155
1597
|
if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
|
|
2156
1598
|
this.ptyProcess.updateMeta(meta, replace);
|
|
2157
1599
|
}
|
|
@@ -2172,7 +1614,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2172
1614
|
this.timeouts.shutdownGrace,
|
|
2173
1615
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2174
1616
|
);
|
|
2175
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1617
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2176
1618
|
|
|
2177
1619
|
try {
|
|
2178
1620
|
if (wasProcessing) {
|
|
@@ -2210,7 +1652,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2210
1652
|
return new Promise((resolve) => {
|
|
2211
1653
|
const startedAt = Date.now();
|
|
2212
1654
|
const timer = setInterval(() => {
|
|
2213
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
1655
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2214
1656
|
clearInterval(timer);
|
|
2215
1657
|
resolve(true);
|
|
2216
1658
|
return;
|
|
@@ -2224,18 +1666,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2224
1666
|
}
|
|
2225
1667
|
|
|
2226
1668
|
shutdown(): void {
|
|
2227
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
1669
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2228
1670
|
this.clearAllTimers();
|
|
2229
1671
|
this.pendingOutputParseChunks = [];
|
|
2230
1672
|
this.pendingTerminalQueryTail = '';
|
|
2231
1673
|
this.ptyOutputChunks = [];
|
|
2232
|
-
this.
|
|
1674
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1675
|
+
this.pendingOutboundQueue = [];
|
|
1676
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2233
1677
|
if (this.ptyProcess) {
|
|
2234
1678
|
this.ptyProcess.write('\x03');
|
|
2235
1679
|
setTimeout(() => {
|
|
2236
1680
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2237
1681
|
this.ptyProcess = null;
|
|
2238
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
1682
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2239
1683
|
this.ready = false;
|
|
2240
1684
|
this.startupParseGate = false;
|
|
2241
1685
|
this.spawnAt = 0;
|
|
@@ -2245,12 +1689,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2245
1689
|
}
|
|
2246
1690
|
|
|
2247
1691
|
detach(): void {
|
|
2248
|
-
this.clearIdleFinishCandidate('detach');
|
|
1692
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2249
1693
|
this.clearAllTimers();
|
|
2250
1694
|
this.pendingOutputParseChunks = [];
|
|
2251
1695
|
this.pendingTerminalQueryTail = '';
|
|
2252
1696
|
this.ptyOutputChunks = [];
|
|
2253
|
-
this.
|
|
1697
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1698
|
+
this.pendingOutboundQueue = [];
|
|
1699
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2254
1700
|
if (this.ptyProcess) {
|
|
2255
1701
|
try {
|
|
2256
1702
|
if (typeof this.ptyProcess.detach === 'function') {
|
|
@@ -2268,82 +1714,128 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2268
1714
|
}
|
|
2269
1715
|
|
|
2270
1716
|
clearHistory(): void {
|
|
2271
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
1717
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2272
1718
|
this.accumulatedBuffer = '';
|
|
2273
1719
|
this.accumulatedRawBuffer = '';
|
|
2274
|
-
this.currentTurnScope = null;
|
|
2275
|
-
this.submitRetryUsed = false;
|
|
2276
|
-
this.submitRetryPromptSnippet = '';
|
|
1720
|
+
this.engine.currentTurnScope = null;
|
|
1721
|
+
this.engine.submitRetryUsed = false;
|
|
1722
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2277
1723
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2278
1724
|
this.pendingOutputParseChunks = [];
|
|
2279
1725
|
this.pendingTerminalQueryTail = '';
|
|
2280
1726
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2281
1727
|
this.ptyOutputChunks = [];
|
|
2282
|
-
if (this.
|
|
2283
|
-
this.
|
|
1728
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1729
|
+
this.pendingOutboundQueue = [];
|
|
1730
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2284
1731
|
this.resetTerminalScreen();
|
|
2285
1732
|
this.ptyProcess?.clearBuffer?.();
|
|
2286
1733
|
this.onStatusChange?.();
|
|
2287
1734
|
}
|
|
2288
1735
|
|
|
2289
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
1736
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2290
1737
|
isReady(): boolean { return this.ready; }
|
|
2291
1738
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
1739
|
+
// ─── State machine property accessors (delegate to engine) ──────────────
|
|
1740
|
+
// These expose engine state for external callers (tests, debug tools, etc.)
|
|
1741
|
+
|
|
1742
|
+
get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
|
|
1743
|
+
set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
|
|
1744
|
+
|
|
1745
|
+
get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
|
|
1746
|
+
set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
|
|
1747
|
+
|
|
1748
|
+
get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
|
|
1749
|
+
set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
|
|
1750
|
+
|
|
1751
|
+
get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
|
|
1752
|
+
set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
|
|
1753
|
+
|
|
1754
|
+
get responseEpoch(): number { return this.engine.responseEpoch; }
|
|
1755
|
+
set responseEpoch(v: number) { this.engine.responseEpoch = v; }
|
|
1756
|
+
|
|
1757
|
+
get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
|
|
1758
|
+
set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
|
|
1759
|
+
|
|
1760
|
+
get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
|
|
1761
|
+
set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
|
|
1762
|
+
|
|
1763
|
+
get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
|
|
1764
|
+
set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
|
|
1765
|
+
|
|
1766
|
+
get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
|
|
1767
|
+
set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
|
|
1768
|
+
|
|
1769
|
+
get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
|
|
1770
|
+
set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
|
|
1771
|
+
|
|
1772
|
+
get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
|
|
1773
|
+
get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
|
|
1774
|
+
|
|
1775
|
+
get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
|
|
1776
|
+
get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
|
|
1777
|
+
|
|
1778
|
+
get finishRetryCount(): number { return this.engine.finishRetryCount; }
|
|
1779
|
+
set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
|
|
1780
|
+
|
|
1781
|
+
get traceSessionId(): string { return this.engine.getTraceSessionId(); }
|
|
1782
|
+
get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
|
|
1783
|
+
get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
|
|
1784
|
+
get traceSeq(): number { return this.engine.getTraceEntries().length; }
|
|
1785
|
+
|
|
1786
|
+
/** Expose engine's evaluateSettled for test access */
|
|
1787
|
+
evaluateSettled(): void {
|
|
1788
|
+
LOG.debug(
|
|
1789
|
+
'CLI',
|
|
1790
|
+
`[${this.cliType}] settled diagnostics delegated to state engine`);
|
|
1791
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1792
|
+
}
|
|
1793
|
+
/** Expose engine's scheduleSettle for test access */
|
|
1794
|
+
scheduleSettle(): void { this.engine.scheduleSettle(); }
|
|
1795
|
+
/** Expose engine's clearIdleFinishCandidate for test access */
|
|
1796
|
+
clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
|
|
1797
|
+
/** Expose engine's finishResponse for test access */
|
|
1798
|
+
finishResponse(): void { this.engine.finishResponse(); }
|
|
1799
|
+
/** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
|
|
1800
|
+
getSnapshot(): CliBufferSnapshot {
|
|
1801
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
1802
|
+
return {
|
|
1803
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
1804
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1805
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
1806
|
+
responseBuffer: this.responseBuffer,
|
|
1807
|
+
screenText,
|
|
1808
|
+
parseScreenText: this.getParseScreenText(screenText),
|
|
1809
|
+
workingDir: this.workingDir,
|
|
1810
|
+
providerSessionId: this.providerSessionId,
|
|
1811
|
+
runtimeSettings: this.runtimeSettings,
|
|
1812
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1813
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1814
|
+
lastOutputAt: this.lastOutputAt,
|
|
1815
|
+
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
1816
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1817
|
+
lastScreenSnapshot: this.lastScreenSnapshot,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
isAlive(): boolean { return this.ptyProcess !== null; }
|
|
1821
|
+
flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
|
|
1822
|
+
|
|
1823
|
+
async writeRaw(data: string | Buffer): Promise<void> {
|
|
1824
|
+
const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
|
1825
|
+
await this.writeToPty(str);
|
|
2298
1826
|
}
|
|
2299
1827
|
|
|
2300
1828
|
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
|
-
}
|
|
1829
|
+
this.engine.resolveModal(buttonIndex);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
1833
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
1837
|
+
isApprovalRecentlyResolved(): boolean {
|
|
1838
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2347
1839
|
}
|
|
2348
1840
|
|
|
2349
1841
|
resize(cols: number, rows: number): void {
|
|
@@ -2357,7 +1849,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2357
1849
|
}
|
|
2358
1850
|
|
|
2359
1851
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2360
|
-
if (this.startupParseGate ||
|
|
1852
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2361
1853
|
try {
|
|
2362
1854
|
const parsed = this.getScriptParsedStatus();
|
|
2363
1855
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2369,18 +1861,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2369
1861
|
getDebugState(): Record<string, any> {
|
|
2370
1862
|
const screenText = sanitizeTerminalText(this.terminalScreen.getText());
|
|
2371
1863
|
const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
2372
|
-
const
|
|
2373
|
-
|
|
1864
|
+
const startupDetectedStatus = this.startupParseGate && !startupModal
|
|
1865
|
+
? this.runDetectStatus(this.recentOutputBuffer || screenText)
|
|
1866
|
+
: null;
|
|
1867
|
+
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2374
1868
|
const parsedDebugState = this.getParsedDebugState();
|
|
2375
1869
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
1870
|
+
const hasFinalAssistant = (p: any) => {
|
|
1871
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1872
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1873
|
+
};
|
|
1874
|
+
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1875
|
+
if (parsedDebugState?.status === 'error') {
|
|
1876
|
+
effectiveStatus = 'error';
|
|
1877
|
+
}
|
|
1878
|
+
// (fix) Mirror the getStatus contract: never surface waiting_approval
|
|
1879
|
+
// without a concrete modal. detectStatus alone can fire while
|
|
1880
|
+
// parseApproval is still null, and a bare debugState waiting_approval
|
|
1881
|
+
// confused dashboards reading the debug payload.
|
|
1882
|
+
const debugEffectiveModal = startupModal || this.engine.activeModal;
|
|
1883
|
+
if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
|
|
1884
|
+
effectiveStatus = 'waiting_approval';
|
|
1885
|
+
}
|
|
1886
|
+
if (
|
|
1887
|
+
effectiveStatus === 'idle'
|
|
1888
|
+
&& parsedDebugState?.status === 'generating'
|
|
1889
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
1890
|
+
) {
|
|
1891
|
+
effectiveStatus = 'generating';
|
|
1892
|
+
}
|
|
2376
1893
|
return {
|
|
2377
1894
|
type: this.cliType,
|
|
2378
1895
|
name: this.cliName,
|
|
2379
1896
|
providerResolution: this.providerResolutionMeta,
|
|
2380
1897
|
status: effectiveStatus,
|
|
2381
1898
|
projectedStatus: effectiveStatus,
|
|
2382
|
-
rawStatus: this.currentStatus,
|
|
2383
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1899
|
+
rawStatus: this.engine.currentStatus,
|
|
1900
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2384
1901
|
ready: effectiveReady,
|
|
2385
1902
|
startupParseGate: this.startupParseGate,
|
|
2386
1903
|
spawnAt: this.spawnAt,
|
|
@@ -2394,38 +1911,47 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2394
1911
|
providerSessionId: parsedDebugState.providerSessionId,
|
|
2395
1912
|
transcriptAuthority: parsedDebugState.transcriptAuthority,
|
|
2396
1913
|
coverage: parsedDebugState.coverage,
|
|
1914
|
+
errorMessage: parsedDebugState.errorMessage,
|
|
1915
|
+
errorReason: parsedDebugState.errorReason,
|
|
2397
1916
|
activeModal: parsedDebugState.activeModal,
|
|
2398
1917
|
messageCount: parsedMessages.length,
|
|
2399
1918
|
} : null,
|
|
2400
1919
|
screenText: screenText.slice(-4000),
|
|
2401
|
-
currentTurnScope: this.currentTurnScope,
|
|
1920
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2402
1921
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2403
1922
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2404
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2405
1923
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2406
1924
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2407
1925
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
2408
1926
|
sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
|
|
2409
1927
|
responseBuffer: this.responseBuffer.slice(-1000),
|
|
1928
|
+
pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
|
|
1929
|
+
id: message.id,
|
|
1930
|
+
role: message.role,
|
|
1931
|
+
content: message.content,
|
|
1932
|
+
queuedAt: message.queuedAt,
|
|
1933
|
+
source: message.source,
|
|
1934
|
+
})),
|
|
1935
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
2410
1936
|
lastOutputAt: this.lastOutputAt,
|
|
2411
1937
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2412
1938
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2413
1939
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2414
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2415
|
-
activeModal: startupModal || this.activeModal,
|
|
2416
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1940
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1941
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
1942
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2417
1943
|
sendDelayMs: this.sendDelayMs,
|
|
2418
1944
|
sendKey: this.sendKey,
|
|
2419
1945
|
submitStrategy: this.submitStrategy,
|
|
2420
1946
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2421
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2422
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1947
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1948
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2423
1949
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2424
1950
|
hasCliScripts: this.hasCliScripts(),
|
|
2425
|
-
scriptNames:
|
|
2426
|
-
traceSessionId: this.
|
|
2427
|
-
traceEntryCount: this.
|
|
2428
|
-
statusHistory: this.
|
|
1951
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1952
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1953
|
+
traceEntryCount: this.engine.getTraceEntries().length,
|
|
1954
|
+
statusHistory: this.engine.getStatusHistory().slice(-30),
|
|
2429
1955
|
timeouts: this.timeouts,
|
|
2430
1956
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2431
1957
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2435,20 +1961,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2435
1961
|
|
|
2436
1962
|
getTraceState(limit = 120): Record<string, any> {
|
|
2437
1963
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
1964
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2438
1965
|
return {
|
|
2439
|
-
sessionId: this.
|
|
1966
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2440
1967
|
providerResolution: this.providerResolutionMeta,
|
|
2441
|
-
entryCount:
|
|
2442
|
-
entries:
|
|
1968
|
+
entryCount: traceEntries.length,
|
|
1969
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2443
1970
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2444
1971
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2445
1972
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2446
1973
|
status: this.projectEffectiveStatus(),
|
|
2447
1974
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2448
|
-
rawStatus: this.currentStatus,
|
|
2449
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2450
|
-
activeModal: this.activeModal,
|
|
2451
|
-
currentTurnScope: this.currentTurnScope,
|
|
1975
|
+
rawStatus: this.engine.currentStatus,
|
|
1976
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1977
|
+
activeModal: this.engine.activeModal,
|
|
1978
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2452
1979
|
messages: [],
|
|
2453
1980
|
};
|
|
2454
1981
|
}
|