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