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