@adhdev/daemon-core 0.9.82-rc.33 → 0.9.82-rc.330
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 +8 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/build-info.d.ts +37 -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 +46 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +115 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +53 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +14 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -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 +279 -1
- package/dist/commands/upgrade-helper.d.ts +12 -0
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +77 -1
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +11 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +23 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/index.d.ts +2 -0
- package/dist/index.d.ts +51 -12
- package/dist/index.js +38509 -13901
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +41320 -16832
- 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/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +46 -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 +174 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
- package/dist/mesh/mesh-events-pending.d.ts +45 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +47 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-init.d.ts +86 -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 +116 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +401 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
- package/dist/mesh/mesh-work-queue.d.ts +207 -5
- 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/chat-message-normalization.d.ts +12 -0
- package/dist/providers/cli-provider-instance.d.ts +61 -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 +13 -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 +69 -0
- package/dist/providers/spec/cli-adapter.d.ts +217 -0
- package/dist/providers/spec/evaluator.d.ts +23 -0
- package/dist/providers/spec/fsm-driver.d.ts +306 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +144 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +180 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +295 -13
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +52 -2
- 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 +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +55 -12
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -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 +47 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1215 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +713 -1141
- 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 +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +100 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +90 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands.ts +1823 -62
- package/src/commands/cli-manager.ts +286 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +332 -122
- package/src/commands/router.ts +7270 -1306
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +81 -30
- package/src/config/chat-history.ts +483 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +387 -5
- 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/cli-detector.ts +28 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +55 -14
- package/src/git/git-diff.ts +53 -0
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +574 -43
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +8 -1
- package/src/git/index.ts +16 -0
- package/src/index.ts +145 -11
- 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/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +220 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +234 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +623 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-events-coordinator.ts +2110 -0
- package/src/mesh/mesh-events-pending.ts +508 -0
- package/src/mesh/mesh-events-stale.ts +290 -0
- package/src/mesh/mesh-events-utils.ts +254 -0
- package/src/mesh/mesh-events.ts +30 -1035
- package/src/mesh/mesh-fast-forward.ts +839 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-init.ts +260 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +589 -102
- package/src/mesh/mesh-missions.ts +248 -0
- package/src/mesh/mesh-reconcile-loop.ts +786 -0
- package/src/mesh/mesh-refine-batch.ts +197 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +1578 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/mesh/mesh-work-queue.ts +874 -141
- 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 +25 -9
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +45 -5
- package/src/providers/cli-provider-instance.ts +986 -99
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- 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 +479 -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 +662 -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 +168 -0
- package/src/providers/spec/cli-adapter.ts +986 -0
- package/src/providers/spec/evaluator.ts +345 -0
- package/src/providers/spec/fsm-driver.ts +993 -0
- package/src/providers/spec/fsm-evaluator.ts +255 -0
- package/src/providers/spec/fsm-loader.ts +120 -0
- package/src/providers/spec/fsm-types.ts +238 -0
- package/src/providers/spec/native-history-executor.ts +1136 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +226 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +374 -13
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +57 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +16 -1
- 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
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import * as os from 'os';
|
|
18
|
-
import type { CliAdapter } from '../cli-adapter-types.js';
|
|
18
|
+
import type { CliAdapter, CliLaunchInfo } 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.
|
|
195
179
|
|
|
196
|
-
// ───
|
|
197
|
-
private
|
|
198
|
-
/**
|
|
199
|
-
|
|
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; }
|
|
184
|
+
|
|
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
|
-
}
|
|
789
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
996
790
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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() || '';
|
|
1026
|
-
|
|
1027
|
-
this.recordTrace('settled', {
|
|
1028
|
-
tail: summarizeCliTraceText(this.settledBuffer, 500),
|
|
1029
|
-
screenText: summarizeCliTraceText(screenText, 1200),
|
|
1030
|
-
detectStatus: status,
|
|
1031
|
-
parsedStatus: parsedStatus || null,
|
|
1032
|
-
parsedMessageCount: parsedMessages.length,
|
|
1033
|
-
parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
|
|
1034
|
-
parsedActiveModal: modal,
|
|
1035
|
-
approval: modal,
|
|
1036
|
-
...buildCliTraceParseSnapshot({
|
|
1037
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1038
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1039
|
-
responseBuffer: this.responseBuffer,
|
|
1040
|
-
partialResponse: this.responseBuffer,
|
|
1041
|
-
scope: this.currentTurnScope,
|
|
1042
|
-
}),
|
|
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.
|
|
815
|
+
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
816
|
+
return session;
|
|
1170
817
|
}
|
|
1171
818
|
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
this.
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
this.idleTimeout = setTimeout(() => {
|
|
1183
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1184
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1185
|
-
this.finishResponse();
|
|
1186
|
-
}
|
|
1187
|
-
}, this.timeouts.generatingIdle);
|
|
1188
|
-
} else {
|
|
1189
|
-
this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
|
|
1190
|
-
}
|
|
1191
|
-
this.onStatusChange?.();
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
if (!inCooldown) {
|
|
1195
|
-
if (!modal) {
|
|
1196
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
this.isWaitingForResponse = true;
|
|
1200
|
-
this.setStatus('waiting_approval', 'script_detect');
|
|
1201
|
-
this.activeModal = modal;
|
|
1202
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1203
|
-
this.armApprovalExitTimeout();
|
|
1204
|
-
this.onStatusChange?.();
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
private applyGenerating(ctx: SettledEvalContext): void {
|
|
1209
|
-
const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
|
|
1210
|
-
this.clearIdleFinishCandidate('generating');
|
|
1211
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1212
|
-
const effectiveScreenText = screenText || this.accumulatedBuffer;
|
|
1213
|
-
const noActiveTurn = !this.currentTurnScope;
|
|
1214
|
-
const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
|
|
1215
|
-
const parsedShowsLiveAssistantProgress = parsedStatus === 'generating'
|
|
1216
|
-
&& !!lastParsedAssistant
|
|
1217
|
-
;
|
|
1218
|
-
if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (prevStatus === 'waiting_approval') {
|
|
1222
|
-
// Transitioned out of approval → generating
|
|
1223
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1224
|
-
this.activeModal = null;
|
|
1225
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1226
|
-
}
|
|
1227
|
-
if (!this.isWaitingForResponse) {
|
|
1228
|
-
this.isWaitingForResponse = true;
|
|
1229
|
-
this.responseBuffer = '';
|
|
1230
|
-
}
|
|
1231
|
-
this.setStatus('generating', 'script_detect');
|
|
1232
|
-
// Reset idle timeout
|
|
1233
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1234
|
-
this.idleTimeout = setTimeout(() => {
|
|
1235
|
-
if (this.isWaitingForResponse) {
|
|
1236
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1237
|
-
this.finishResponse();
|
|
1238
|
-
}
|
|
1239
|
-
}, this.timeouts.generatingIdle);
|
|
1240
|
-
this.onStatusChange?.();
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
private applyIdle(ctx: SettledEvalContext, now: number): void {
|
|
1244
|
-
const { modal, lastParsedAssistant, prevStatus } = ctx;
|
|
1245
|
-
if (prevStatus === 'waiting_approval') {
|
|
1246
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1247
|
-
this.activeModal = null;
|
|
1248
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1249
|
-
this.setStatus('idle', 'approval_prompt_gone_script_idle');
|
|
1250
|
-
}
|
|
1251
|
-
if (!this.isWaitingForResponse) {
|
|
1252
|
-
if (prevStatus !== 'idle') {
|
|
1253
|
-
this.clearIdleFinishCandidate('idle_without_response');
|
|
1254
|
-
this.setStatus('idle', 'script_detect');
|
|
1255
|
-
this.onStatusChange?.();
|
|
1256
|
-
}
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
1260
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
1261
|
-
const hasAssistantTurn = !!lastParsedAssistant;
|
|
1262
|
-
const assistantLength = lastParsedAssistant?.content?.length || 0;
|
|
1263
|
-
const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
|
|
1264
|
-
const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
|
|
1265
|
-
const idleReady = !modal
|
|
1266
|
-
&& hasAssistantTurn
|
|
1267
|
-
&& quietForMs >= idleQuietThresholdMs
|
|
1268
|
-
&& screenStableMs >= idleFinishConfirmMs;
|
|
1269
|
-
const candidate = this.idleFinishCandidate;
|
|
1270
|
-
const candidateQuiet = !!candidate
|
|
1271
|
-
&& candidate.responseEpoch === this.responseEpoch
|
|
1272
|
-
&& candidate.lastOutputAt === this.lastOutputAt
|
|
1273
|
-
&& candidate.lastScreenChangeAt === this.lastScreenChangeAt
|
|
1274
|
-
&& assistantLength >= candidate.assistantLength
|
|
1275
|
-
&& (now - candidate.armedAt) >= idleFinishConfirmMs;
|
|
1276
|
-
|
|
1277
|
-
this.recordTrace('idle_decision', {
|
|
1278
|
-
quietForMs,
|
|
1279
|
-
screenStableMs,
|
|
1280
|
-
hasAssistantTurn,
|
|
1281
|
-
assistantLength,
|
|
1282
|
-
hasModal: !!modal,
|
|
1283
|
-
idleQuietThresholdMs,
|
|
1284
|
-
idleStableThresholdMs: idleFinishConfirmMs,
|
|
1285
|
-
idleReady,
|
|
1286
|
-
idleFinishConfirmMs,
|
|
1287
|
-
idleFinishCandidate: candidate,
|
|
1288
|
-
candidateQuiet,
|
|
1289
|
-
canFinishImmediately: idleReady && candidateQuiet,
|
|
1290
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
1291
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1292
|
-
...buildCliTraceParseSnapshot({
|
|
1293
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1294
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1295
|
-
responseBuffer: this.responseBuffer,
|
|
1296
|
-
partialResponse: this.responseBuffer,
|
|
1297
|
-
scope: this.currentTurnScope,
|
|
1298
|
-
}),
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
if (idleReady && candidateQuiet) {
|
|
1302
|
-
this.clearIdleFinishCandidate('finish_response');
|
|
1303
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1304
|
-
this.finishResponse();
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (idleReady) {
|
|
1309
|
-
if (!candidate) {
|
|
1310
|
-
this.armIdleFinishCandidate(assistantLength);
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
} else {
|
|
1314
|
-
this.clearIdleFinishCandidate('idle_not_ready');
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1318
|
-
this.idleTimeout = setTimeout(() => {
|
|
1319
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1320
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1321
|
-
this.clearIdleFinishCandidate('idle_timeout_finish');
|
|
1322
|
-
this.finishResponse();
|
|
1323
|
-
}
|
|
1324
|
-
}, this.timeouts.idleFinish);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
private finishResponse(): void {
|
|
1328
|
-
if (this.submitPendingUntil > Date.now()) return;
|
|
1329
|
-
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
1330
|
-
this.clearIdleFinishCandidate('finish_response_enter');
|
|
1331
|
-
this.recordTrace('finish_response', {
|
|
1332
|
-
...buildCliTraceParseSnapshot({
|
|
1333
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1334
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1335
|
-
responseBuffer: this.responseBuffer,
|
|
1336
|
-
partialResponse: this.responseBuffer,
|
|
1337
|
-
scope: this.currentTurnScope,
|
|
1338
|
-
}),
|
|
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,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1583
940
|
messages: [],
|
|
1584
941
|
workingDir: this.workingDir,
|
|
1585
942
|
activeModal: effectiveModal,
|
|
1586
|
-
|
|
1587
|
-
|
|
943
|
+
approvalEntrySeq: this.engine.approvalEntrySeq,
|
|
944
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
945
|
+
pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
|
|
946
|
+
id: message.id,
|
|
947
|
+
role: message.role,
|
|
948
|
+
content: message.content,
|
|
949
|
+
queuedAt: message.queuedAt,
|
|
950
|
+
source: message.source,
|
|
951
|
+
})),
|
|
952
|
+
errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
|
|
953
|
+
errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
|
|
954
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
955
|
+
lastOutputAt: this.lastOutputAt,
|
|
956
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1588
957
|
...(bufferState ? { bufferState } : {}),
|
|
1589
958
|
};
|
|
1590
959
|
}
|
|
@@ -1600,15 +969,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1600
969
|
const cached = this.parsedStatusCache;
|
|
1601
970
|
const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
|
|
1602
971
|
if (
|
|
1603
|
-
|
|
972
|
+
!this.providerOwnsTranscript()
|
|
973
|
+
&& cached
|
|
1604
974
|
&& cached.responseBuffer === this.responseBuffer
|
|
1605
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
975
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1606
976
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1607
977
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1608
978
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1609
979
|
&& cached.screenText === parseScreenText
|
|
1610
|
-
&& cached.currentStatus === this.currentStatus
|
|
1611
|
-
&& cached.activeModal === this.activeModal
|
|
980
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
981
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1612
982
|
&& cached.cliName === this.cliName
|
|
1613
983
|
) {
|
|
1614
984
|
return cached.result;
|
|
@@ -1623,14 +993,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1623
993
|
const bufferState = this.getBufferState();
|
|
1624
994
|
const result = {
|
|
1625
995
|
id: (parsed as any).id || 'cli_session',
|
|
1626
|
-
status: parsed.status || this.currentStatus,
|
|
996
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1627
997
|
title: (parsed as any).title || this.cliName,
|
|
1628
998
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1629
999
|
scope: null,
|
|
1630
1000
|
lastOutputAt: this.lastOutputAt,
|
|
1631
1001
|
}),
|
|
1632
1002
|
activeModal,
|
|
1633
|
-
providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
|
|
1003
|
+
providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
|
|
1004
|
+
errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
|
|
1005
|
+
? (parsed as any).errorMessage.trim()
|
|
1006
|
+
: undefined,
|
|
1007
|
+
errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
|
|
1008
|
+
? (parsed as any).errorReason.trim()
|
|
1009
|
+
: undefined,
|
|
1634
1010
|
...(bufferState ? { bufferState } : {}),
|
|
1635
1011
|
...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
|
|
1636
1012
|
? { transcriptAuthority: (parsed as any).transcriptAuthority }
|
|
@@ -1642,13 +1018,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1642
1018
|
|
|
1643
1019
|
this.parsedStatusCache = {
|
|
1644
1020
|
responseBuffer: this.responseBuffer,
|
|
1645
|
-
currentTurnScope: this.currentTurnScope,
|
|
1021
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1646
1022
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1647
1023
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1648
1024
|
accumulatedRawBufferKey,
|
|
1649
1025
|
screenText: parseScreenText,
|
|
1650
|
-
currentStatus: this.currentStatus,
|
|
1651
|
-
activeModal: this.activeModal,
|
|
1026
|
+
currentStatus: this.engine.currentStatus,
|
|
1027
|
+
activeModal: this.engine.activeModal,
|
|
1652
1028
|
cliName: this.cliName,
|
|
1653
1029
|
result,
|
|
1654
1030
|
};
|
|
@@ -1656,22 +1032,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1656
1032
|
}
|
|
1657
1033
|
|
|
1658
1034
|
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
1035
|
const input = buildCliParseInput({
|
|
1664
1036
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1665
1037
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1666
1038
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1667
1039
|
terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
|
|
1040
|
+
workingDir: this.workingDir,
|
|
1041
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1042
|
+
historySessionId: this.providerSessionId || undefined,
|
|
1668
1043
|
baseMessages: [],
|
|
1669
1044
|
partialResponse: this.responseBuffer,
|
|
1670
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1671
|
-
scope: this.currentTurnScope,
|
|
1045
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1046
|
+
scope: this.engine.currentTurnScope,
|
|
1672
1047
|
runtimeSettings: this.runtimeSettings,
|
|
1048
|
+
spawnAt: this.spawnAt,
|
|
1673
1049
|
});
|
|
1674
|
-
return await Promise.resolve(this.
|
|
1050
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1675
1051
|
...input,
|
|
1676
1052
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1677
1053
|
}));
|
|
@@ -1683,7 +1059,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1683
1059
|
|
|
1684
1060
|
/** Whether this adapter has CLI scripts loaded */
|
|
1685
1061
|
hasCliScripts(): boolean {
|
|
1686
|
-
return
|
|
1062
|
+
return this.runner.hasDetectStatus();
|
|
1687
1063
|
}
|
|
1688
1064
|
|
|
1689
1065
|
/**
|
|
@@ -1692,24 +1068,29 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1692
1068
|
*/
|
|
1693
1069
|
async resolveAction(data: any): Promise<void> {
|
|
1694
1070
|
let promptText = '';
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1071
|
+
try {
|
|
1072
|
+
promptText = this.runner.invokeByName('resolveAction', data);
|
|
1073
|
+
} catch {
|
|
1074
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
|
|
1075
|
+
return;
|
|
1701
1076
|
}
|
|
1702
1077
|
if (!promptText) {
|
|
1703
|
-
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not
|
|
1078
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
|
|
1704
1079
|
return;
|
|
1705
1080
|
}
|
|
1706
1081
|
await this.sendMessage(promptText);
|
|
1707
1082
|
}
|
|
1708
1083
|
|
|
1084
|
+
async setInteractivePromptResponse(_response: InteractivePromptResponse): Promise<void> {
|
|
1085
|
+
// Legacy TUI providers do not currently expose an interactive prompt
|
|
1086
|
+
// protocol. Spec-backed claude-cli implements the first real wiring.
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1709
1089
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
1710
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
1711
|
-
if (this.hasActionableApproval()) return false;
|
|
1712
|
-
|
|
1090
|
+
if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
|
|
1091
|
+
if (this.engine.hasActionableApproval()) return false;
|
|
1092
|
+
// If there's already meaningful response content beyond the echoed prompt, not stuck
|
|
1093
|
+
if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
|
|
1713
1094
|
const screenText = this.terminalScreen.getText();
|
|
1714
1095
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
1715
1096
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -1718,20 +1099,41 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1718
1099
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
1719
1100
|
}
|
|
1720
1101
|
|
|
1102
|
+
private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
|
|
1103
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
1104
|
+
if (!raw) return false;
|
|
1105
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1106
|
+
if (!normalizedPrompt) return true;
|
|
1107
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
1108
|
+
if (!normalizedBuffer) return false;
|
|
1109
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1110
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1111
|
+
const remainder = normalizedBuffer
|
|
1112
|
+
.slice(normalizedPrompt.length)
|
|
1113
|
+
.replace(/[─═\-]+/g, '')
|
|
1114
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1115
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1116
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1117
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1118
|
+
.replace(/esctointerrupt/gi, '')
|
|
1119
|
+
.replace(/❯/g, '')
|
|
1120
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1121
|
+
.trim();
|
|
1122
|
+
return remainder.length > 0;
|
|
1123
|
+
}
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1721
1127
|
private async writeToPty(data: string): Promise<void> {
|
|
1722
1128
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1723
1129
|
await this.ptyProcess.write(data);
|
|
1724
1130
|
}
|
|
1725
1131
|
|
|
1726
1132
|
private resetPendingSendState(reason: string): void {
|
|
1727
|
-
this.isWaitingForResponse = false;
|
|
1728
1133
|
this.responseBuffer = '';
|
|
1729
|
-
this.currentTurnScope = null;
|
|
1730
|
-
this.submitPendingUntil = 0;
|
|
1731
|
-
this.clearIdleFinishCandidate(reason);
|
|
1732
1134
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1733
|
-
|
|
1734
|
-
|
|
1135
|
+
this.engine.resetActiveTurnState();
|
|
1136
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
1735
1137
|
}
|
|
1736
1138
|
|
|
1737
1139
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
@@ -1748,42 +1150,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1748
1150
|
}
|
|
1749
1151
|
this.responseTimeout = setTimeout(() => {
|
|
1750
1152
|
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
|
-
});
|
|
1153
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
1768
1154
|
|
|
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
|
|
1155
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
1773
1156
|
// normal settled parser instead and keep the turn open unless the provider
|
|
1774
1157
|
// 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
|
-
});
|
|
1158
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1159
|
+
|
|
1160
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
1787
1161
|
this.armResponseTimeout();
|
|
1788
1162
|
}
|
|
1789
1163
|
}, timeoutMs);
|
|
@@ -1798,34 +1172,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1798
1172
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
1799
1173
|
this.submitRetryTimer = null;
|
|
1800
1174
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1801
|
-
|
|
1802
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1175
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1803
1176
|
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
1177
|
this.writeSubmitKeyForRetry('submit_retry');
|
|
1811
|
-
if (attempt >= 3) { this.submitRetryUsed = true; return; }
|
|
1178
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
1812
1179
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
1813
1180
|
}
|
|
1814
1181
|
|
|
1815
1182
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
1816
1183
|
this.submitRetryTimer = null;
|
|
1817
1184
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1818
|
-
|
|
1819
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1185
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1820
1186
|
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
1187
|
this.writeSubmitKeyForRetry('immediate_retry');
|
|
1828
|
-
this.submitRetryUsed = true;
|
|
1188
|
+
this.engine.submitRetryUsed = true;
|
|
1829
1189
|
}
|
|
1830
1190
|
|
|
1831
1191
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -1833,13 +1193,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1833
1193
|
completion.resolveOnce();
|
|
1834
1194
|
return;
|
|
1835
1195
|
}
|
|
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
|
-
});
|
|
1196
|
+
this.engine.submitPendingUntil = 0;
|
|
1843
1197
|
void this.writeToPty(this.sendKey).then(() => {
|
|
1844
1198
|
this.commitSendUserTurn(state);
|
|
1845
1199
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -1849,13 +1203,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1849
1203
|
}
|
|
1850
1204
|
|
|
1851
1205
|
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
|
-
});
|
|
1206
|
+
this.engine.submitPendingUntil = 0;
|
|
1859
1207
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
1860
1208
|
this.commitSendUserTurn(state);
|
|
1861
1209
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -1899,7 +1247,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1899
1247
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
1900
1248
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
1901
1249
|
};
|
|
1902
|
-
|
|
1250
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1251
|
+
|
|
1903
1252
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
1904
1253
|
// At this point the prompt text write already completed. Rejecting without
|
|
1905
1254
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -1924,12 +1273,110 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1924
1273
|
), 50);
|
|
1925
1274
|
}
|
|
1926
1275
|
|
|
1927
|
-
async sendMessage(text: string): Promise<void> {
|
|
1276
|
+
async sendMessage(text: string, options: { force?: boolean } = {}): Promise<void> {
|
|
1277
|
+
if (options.force === true) {
|
|
1278
|
+
await this.forceSendMessage(text);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
await this.sendMessageNow(text, true);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async forceSendMessage(text: string): Promise<void> {
|
|
1285
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1286
|
+
const content = String(text || '');
|
|
1287
|
+
if (!content.trim()) return;
|
|
1288
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
|
|
1289
|
+
await this.writeToPty(content + this.sendKey);
|
|
1290
|
+
this.onStatusChange?.();
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private enqueuePendingOutboundMessage(text: string, reason: string): void {
|
|
1294
|
+
const content = String(text || '');
|
|
1295
|
+
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
1296
|
+
if (duplicate) {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const queuedAt = Date.now();
|
|
1300
|
+
const message: PendingOutboundMessage = {
|
|
1301
|
+
id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
|
|
1302
|
+
role: 'user',
|
|
1303
|
+
content,
|
|
1304
|
+
queuedAt,
|
|
1305
|
+
source: 'sendMessage',
|
|
1306
|
+
};
|
|
1307
|
+
this.pendingOutboundQueue.push(message);
|
|
1308
|
+
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
1309
|
+
this.onStatusChange?.();
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
1313
|
+
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
1314
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
1315
|
+
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1316
|
+
? String(parsedStatusBeforeSend.status)
|
|
1317
|
+
: '';
|
|
1318
|
+
const hasFinalAssistant = (p: any) => {
|
|
1319
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1320
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1321
|
+
};
|
|
1322
|
+
if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
|
|
1323
|
+
if (this.engine.currentStatus === 'generating') return 'current_status_generating';
|
|
1324
|
+
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating') {
|
|
1325
|
+
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1326
|
+
const parsedHasActionableModal = Boolean(
|
|
1327
|
+
parsedModal
|
|
1328
|
+
&& Array.isArray(parsedModal.buttons)
|
|
1329
|
+
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1330
|
+
);
|
|
1331
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1332
|
+
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1333
|
+
&& !this.engine.isWaitingForResponse
|
|
1334
|
+
&& !this.engine.currentTurnScope
|
|
1335
|
+
&& !this.engine.hasActionableApproval()
|
|
1336
|
+
&& !parsedHasActionableModal;
|
|
1337
|
+
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
1338
|
+
}
|
|
1339
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
private schedulePendingOutboundFlush(delayMs = 0): void {
|
|
1344
|
+
if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
|
|
1345
|
+
this.pendingOutboundFlushTimer = setTimeout(() => {
|
|
1346
|
+
this.pendingOutboundFlushTimer = null;
|
|
1347
|
+
void this.flushPendingOutboundQueue();
|
|
1348
|
+
}, Math.max(0, delayMs));
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
private async flushPendingOutboundQueue(): Promise<void> {
|
|
1352
|
+
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
1353
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
1354
|
+
this.pendingOutboundFlushInFlight = true;
|
|
1355
|
+
try {
|
|
1356
|
+
while (this.pendingOutboundQueue.length > 0) {
|
|
1357
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
1358
|
+
const next = this.pendingOutboundQueue[0];
|
|
1359
|
+
try {
|
|
1360
|
+
await this.sendMessageNow(next.content, false);
|
|
1361
|
+
this.pendingOutboundQueue.shift();
|
|
1362
|
+
this.onStatusChange?.();
|
|
1363
|
+
} catch (error: any) {
|
|
1364
|
+
LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
|
|
1365
|
+
this.schedulePendingOutboundFlush(1000);
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
} finally {
|
|
1370
|
+
this.pendingOutboundFlushInFlight = false;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
|
|
1928
1375
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1929
1376
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
1930
1377
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
1931
|
-
&& this.isWaitingForResponse
|
|
1932
|
-
&& !this.hasActionableApproval();
|
|
1378
|
+
&& this.engine.isWaitingForResponse
|
|
1379
|
+
&& !this.engine.hasActionableApproval();
|
|
1933
1380
|
if (this.startupParseGate) {
|
|
1934
1381
|
const deadline = Date.now() + 10000;
|
|
1935
1382
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -1937,76 +1384,85 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1937
1384
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1938
1385
|
}
|
|
1939
1386
|
}
|
|
1387
|
+
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
1388
|
+
? (() => {
|
|
1389
|
+
try {
|
|
1390
|
+
return this.getScriptParsedStatus?.() || null;
|
|
1391
|
+
} catch {
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
})()
|
|
1395
|
+
: null;
|
|
1396
|
+
const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
|
|
1397
|
+
if (allowQueue && queueReason) {
|
|
1398
|
+
this.enqueuePendingOutboundMessage(text, queueReason);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1940
1401
|
if (!allowInterventionPrompt) {
|
|
1941
1402
|
await this.waitForInteractivePrompt();
|
|
1942
1403
|
}
|
|
1943
1404
|
if (!this.ready) {
|
|
1944
1405
|
this.resolveStartupState('send_precheck');
|
|
1945
|
-
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1406
|
+
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
1946
1407
|
this.ready = true;
|
|
1947
1408
|
this.startupParseGate = false;
|
|
1409
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1948
1410
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
1949
1411
|
}
|
|
1950
1412
|
}
|
|
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;
|
|
1413
|
+
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
1961
1414
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1962
1415
|
? String(parsedStatusBeforeSend.status)
|
|
1963
1416
|
: '';
|
|
1964
|
-
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating')) {
|
|
1417
|
+
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating')) {
|
|
1965
1418
|
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1966
1419
|
const parsedHasActionableModal = Boolean(
|
|
1967
1420
|
parsedModal
|
|
1968
1421
|
&& Array.isArray(parsedModal.buttons)
|
|
1969
1422
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1970
1423
|
);
|
|
1971
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1424
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1972
1425
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1973
|
-
&& !this.isWaitingForResponse
|
|
1974
|
-
&& !this.currentTurnScope
|
|
1975
|
-
&& !this.hasActionableApproval()
|
|
1426
|
+
&& !this.engine.isWaitingForResponse
|
|
1427
|
+
&& !this.engine.currentTurnScope
|
|
1428
|
+
&& !this.engine.hasActionableApproval()
|
|
1976
1429
|
&& !parsedHasActionableModal;
|
|
1977
1430
|
if (!terminalLooksIdle) {
|
|
1431
|
+
if (allowQueue) {
|
|
1432
|
+
this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1978
1435
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1979
1436
|
}
|
|
1980
1437
|
}
|
|
1981
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1982
|
-
|
|
1438
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1439
|
+
const snap = this.getSnapshot();
|
|
1440
|
+
if (
|
|
1441
|
+
!this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
|
|
1442
|
+
&& !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
|
|
1443
|
+
) {
|
|
1444
|
+
if (allowQueue) {
|
|
1445
|
+
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1983
1448
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1984
1449
|
}
|
|
1985
1450
|
}
|
|
1986
|
-
this.isWaitingForResponse = true;
|
|
1987
1451
|
this.responseBuffer = '';
|
|
1988
|
-
|
|
1989
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1990
|
-
this.clearIdleFinishCandidate('send_message');
|
|
1991
|
-
this.currentTurnScope = {
|
|
1452
|
+
const turnScope: TurnParseScope = {
|
|
1992
1453
|
prompt: text,
|
|
1993
1454
|
startedAt: Date.now(),
|
|
1994
1455
|
bufferStart: this.accumulatedBuffer.length,
|
|
1995
1456
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
1996
1457
|
};
|
|
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);
|
|
1458
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2006
1459
|
if (this.submitRetryTimer) {
|
|
2007
1460
|
clearTimeout(this.submitRetryTimer);
|
|
2008
1461
|
this.submitRetryTimer = null;
|
|
2009
1462
|
}
|
|
1463
|
+
this.engine.onTurnStarted(turnScope);
|
|
1464
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1465
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2010
1466
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2011
1467
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2012
1468
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2019,12 +1475,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2019
1475
|
retryDelayMs,
|
|
2020
1476
|
didCommitUserTurn: false,
|
|
2021
1477
|
};
|
|
2022
|
-
|
|
2023
|
-
clearTimeout(this.settleTimer);
|
|
2024
|
-
this.settleTimer = null;
|
|
2025
|
-
}
|
|
2026
|
-
this.responseEpoch += 1;
|
|
2027
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1478
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2028
1479
|
await new Promise<void>((resolve, reject) => {
|
|
2029
1480
|
let resolved = false;
|
|
2030
1481
|
const completion: SendMessageCompletion = {
|
|
@@ -2047,24 +1498,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2047
1498
|
}
|
|
2048
1499
|
|
|
2049
1500
|
if (submitDelayMs > 0) {
|
|
2050
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1501
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2051
1502
|
}
|
|
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
1503
|
const submitStartedAt = Date.now();
|
|
2059
1504
|
void this.writeToPty(text).then(
|
|
2060
1505
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2061
1506
|
completion.rejectOnce,
|
|
2062
1507
|
);
|
|
2063
1508
|
});
|
|
1509
|
+
// Schedule settle after successful send
|
|
1510
|
+
this.engine.scheduleSettle();
|
|
2064
1511
|
}
|
|
2065
1512
|
|
|
2066
1513
|
getPartialResponse(): string {
|
|
2067
|
-
if (!this.isWaitingForResponse) return '';
|
|
1514
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2068
1515
|
return this.responseBuffer;
|
|
2069
1516
|
}
|
|
2070
1517
|
|
|
@@ -2077,10 +1524,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2077
1524
|
cliType: this.cliType,
|
|
2078
1525
|
cliName: this.cliName,
|
|
2079
1526
|
workingDir: this.workingDir,
|
|
2080
|
-
currentStatus: this.currentStatus,
|
|
1527
|
+
currentStatus: this.engine.currentStatus,
|
|
2081
1528
|
ready: this.ready,
|
|
2082
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2083
|
-
activeModal: this.activeModal,
|
|
1529
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1530
|
+
activeModal: this.engine.activeModal,
|
|
2084
1531
|
parseErrorMessage: this.parseErrorMessage,
|
|
2085
1532
|
messageCounts: {
|
|
2086
1533
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2106,10 +1553,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2106
1553
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2107
1554
|
},
|
|
2108
1555
|
parser: {
|
|
2109
|
-
scriptNames:
|
|
2110
|
-
traceSessionId: this.
|
|
2111
|
-
traceSeq: this.
|
|
2112
|
-
currentTurnScope: this.currentTurnScope,
|
|
1556
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1557
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1558
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1559
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2113
1560
|
parsedStatusCache: parsedResult
|
|
2114
1561
|
? {
|
|
2115
1562
|
id: parsedResult.id,
|
|
@@ -2122,26 +1569,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2122
1569
|
activeModal: parsedResult.activeModal,
|
|
2123
1570
|
}
|
|
2124
1571
|
: null,
|
|
2125
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2126
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1572
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1573
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2127
1574
|
},
|
|
2128
1575
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2129
|
-
statusHistory: this.
|
|
2130
|
-
traceEntries: this.
|
|
1576
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1577
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2131
1578
|
timing: {
|
|
2132
1579
|
spawnAt: this.spawnAt,
|
|
2133
1580
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2134
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2135
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2136
|
-
responseEpoch: this.responseEpoch,
|
|
1581
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1582
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1583
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2137
1584
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2138
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1585
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2139
1586
|
},
|
|
2140
1587
|
finish: {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1588
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1589
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1590
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2145
1591
|
},
|
|
2146
1592
|
};
|
|
2147
1593
|
}
|
|
@@ -2151,7 +1597,48 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2151
1597
|
return this.ptyProcess.getMetadata();
|
|
2152
1598
|
}
|
|
2153
1599
|
|
|
1600
|
+
/**
|
|
1601
|
+
* Launch metadata for the dashboard Session info panel. Re-derives the spawn plan
|
|
1602
|
+
* (pure — same inputs the live PTY was spawned with) so the dashboard sees the
|
|
1603
|
+
* resolved binary, full arg vector, and cwd without us having to persist it.
|
|
1604
|
+
* extraEnv values are intentionally dropped (keys only) so secrets passed at
|
|
1605
|
+
* launch time never reach the dashboard.
|
|
1606
|
+
*/
|
|
1607
|
+
getLaunchInfo(): CliLaunchInfo {
|
|
1608
|
+
let command: string | undefined;
|
|
1609
|
+
let args: string[] = [...this.extraArgs];
|
|
1610
|
+
try {
|
|
1611
|
+
const plan = resolveCliSpawnPlan({
|
|
1612
|
+
provider: this.provider,
|
|
1613
|
+
runtimeSettings: this.runtimeSettings,
|
|
1614
|
+
workingDir: this.workingDir,
|
|
1615
|
+
extraArgs: this.extraArgs,
|
|
1616
|
+
extraEnv: this.extraEnv,
|
|
1617
|
+
});
|
|
1618
|
+
command = plan.binaryPath;
|
|
1619
|
+
args = plan.allArgs;
|
|
1620
|
+
} catch {
|
|
1621
|
+
// Spawn plan resolution can throw before the binary is resolvable
|
|
1622
|
+
// (e.g. CLI not installed). Fall back to the raw extra args so the
|
|
1623
|
+
// panel still shows what it can rather than failing the whole call.
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
command,
|
|
1627
|
+
args,
|
|
1628
|
+
extraArgs: [...this.extraArgs],
|
|
1629
|
+
cwd: this.workingDir,
|
|
1630
|
+
extraEnvKeys: Object.keys(this.extraEnv || {}),
|
|
1631
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
2154
1635
|
updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
|
|
1636
|
+
const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
|
|
1637
|
+
? meta.providerSessionId.trim()
|
|
1638
|
+
: '';
|
|
1639
|
+
if (nextProviderSessionId) {
|
|
1640
|
+
this.providerSessionId = nextProviderSessionId;
|
|
1641
|
+
}
|
|
2155
1642
|
if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
|
|
2156
1643
|
this.ptyProcess.updateMeta(meta, replace);
|
|
2157
1644
|
}
|
|
@@ -2172,7 +1659,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2172
1659
|
this.timeouts.shutdownGrace,
|
|
2173
1660
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2174
1661
|
);
|
|
2175
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1662
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2176
1663
|
|
|
2177
1664
|
try {
|
|
2178
1665
|
if (wasProcessing) {
|
|
@@ -2210,7 +1697,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2210
1697
|
return new Promise((resolve) => {
|
|
2211
1698
|
const startedAt = Date.now();
|
|
2212
1699
|
const timer = setInterval(() => {
|
|
2213
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
1700
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2214
1701
|
clearInterval(timer);
|
|
2215
1702
|
resolve(true);
|
|
2216
1703
|
return;
|
|
@@ -2224,18 +1711,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2224
1711
|
}
|
|
2225
1712
|
|
|
2226
1713
|
shutdown(): void {
|
|
2227
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
1714
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2228
1715
|
this.clearAllTimers();
|
|
2229
1716
|
this.pendingOutputParseChunks = [];
|
|
2230
1717
|
this.pendingTerminalQueryTail = '';
|
|
2231
1718
|
this.ptyOutputChunks = [];
|
|
2232
|
-
this.
|
|
1719
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1720
|
+
this.pendingOutboundQueue = [];
|
|
1721
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2233
1722
|
if (this.ptyProcess) {
|
|
2234
1723
|
this.ptyProcess.write('\x03');
|
|
2235
1724
|
setTimeout(() => {
|
|
2236
1725
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2237
1726
|
this.ptyProcess = null;
|
|
2238
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
1727
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2239
1728
|
this.ready = false;
|
|
2240
1729
|
this.startupParseGate = false;
|
|
2241
1730
|
this.spawnAt = 0;
|
|
@@ -2245,12 +1734,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2245
1734
|
}
|
|
2246
1735
|
|
|
2247
1736
|
detach(): void {
|
|
2248
|
-
this.clearIdleFinishCandidate('detach');
|
|
1737
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2249
1738
|
this.clearAllTimers();
|
|
2250
1739
|
this.pendingOutputParseChunks = [];
|
|
2251
1740
|
this.pendingTerminalQueryTail = '';
|
|
2252
1741
|
this.ptyOutputChunks = [];
|
|
2253
|
-
this.
|
|
1742
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1743
|
+
this.pendingOutboundQueue = [];
|
|
1744
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2254
1745
|
if (this.ptyProcess) {
|
|
2255
1746
|
try {
|
|
2256
1747
|
if (typeof this.ptyProcess.detach === 'function') {
|
|
@@ -2268,82 +1759,128 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2268
1759
|
}
|
|
2269
1760
|
|
|
2270
1761
|
clearHistory(): void {
|
|
2271
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
1762
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2272
1763
|
this.accumulatedBuffer = '';
|
|
2273
1764
|
this.accumulatedRawBuffer = '';
|
|
2274
|
-
this.currentTurnScope = null;
|
|
2275
|
-
this.submitRetryUsed = false;
|
|
2276
|
-
this.submitRetryPromptSnippet = '';
|
|
1765
|
+
this.engine.currentTurnScope = null;
|
|
1766
|
+
this.engine.submitRetryUsed = false;
|
|
1767
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2277
1768
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2278
1769
|
this.pendingOutputParseChunks = [];
|
|
2279
1770
|
this.pendingTerminalQueryTail = '';
|
|
2280
1771
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2281
1772
|
this.ptyOutputChunks = [];
|
|
2282
|
-
if (this.
|
|
2283
|
-
this.
|
|
1773
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1774
|
+
this.pendingOutboundQueue = [];
|
|
1775
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2284
1776
|
this.resetTerminalScreen();
|
|
2285
1777
|
this.ptyProcess?.clearBuffer?.();
|
|
2286
1778
|
this.onStatusChange?.();
|
|
2287
1779
|
}
|
|
2288
1780
|
|
|
2289
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
1781
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2290
1782
|
isReady(): boolean { return this.ready; }
|
|
2291
1783
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
1784
|
+
// ─── State machine property accessors (delegate to engine) ──────────────
|
|
1785
|
+
// These expose engine state for external callers (tests, debug tools, etc.)
|
|
1786
|
+
|
|
1787
|
+
get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
|
|
1788
|
+
set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
|
|
1789
|
+
|
|
1790
|
+
get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
|
|
1791
|
+
set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
|
|
1792
|
+
|
|
1793
|
+
get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
|
|
1794
|
+
set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
|
|
1795
|
+
|
|
1796
|
+
get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
|
|
1797
|
+
set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
|
|
1798
|
+
|
|
1799
|
+
get responseEpoch(): number { return this.engine.responseEpoch; }
|
|
1800
|
+
set responseEpoch(v: number) { this.engine.responseEpoch = v; }
|
|
1801
|
+
|
|
1802
|
+
get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
|
|
1803
|
+
set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
|
|
1804
|
+
|
|
1805
|
+
get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
|
|
1806
|
+
set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
|
|
1807
|
+
|
|
1808
|
+
get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
|
|
1809
|
+
set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
|
|
1810
|
+
|
|
1811
|
+
get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
|
|
1812
|
+
set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
|
|
1813
|
+
|
|
1814
|
+
get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
|
|
1815
|
+
set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
|
|
1816
|
+
|
|
1817
|
+
get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
|
|
1818
|
+
get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
|
|
1819
|
+
|
|
1820
|
+
get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
|
|
1821
|
+
get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
|
|
1822
|
+
|
|
1823
|
+
get finishRetryCount(): number { return this.engine.finishRetryCount; }
|
|
1824
|
+
set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
|
|
1825
|
+
|
|
1826
|
+
get traceSessionId(): string { return this.engine.getTraceSessionId(); }
|
|
1827
|
+
get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
|
|
1828
|
+
get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
|
|
1829
|
+
get traceSeq(): number { return this.engine.getTraceEntries().length; }
|
|
1830
|
+
|
|
1831
|
+
/** Expose engine's evaluateSettled for test access */
|
|
1832
|
+
evaluateSettled(): void {
|
|
1833
|
+
LOG.debug(
|
|
1834
|
+
'CLI',
|
|
1835
|
+
`[${this.cliType}] settled diagnostics delegated to state engine`);
|
|
1836
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1837
|
+
}
|
|
1838
|
+
/** Expose engine's scheduleSettle for test access */
|
|
1839
|
+
scheduleSettle(): void { this.engine.scheduleSettle(); }
|
|
1840
|
+
/** Expose engine's clearIdleFinishCandidate for test access */
|
|
1841
|
+
clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
|
|
1842
|
+
/** Expose engine's finishResponse for test access */
|
|
1843
|
+
finishResponse(): void { this.engine.finishResponse(); }
|
|
1844
|
+
/** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
|
|
1845
|
+
getSnapshot(): CliBufferSnapshot {
|
|
1846
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
1847
|
+
return {
|
|
1848
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
1849
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1850
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
1851
|
+
responseBuffer: this.responseBuffer,
|
|
1852
|
+
screenText,
|
|
1853
|
+
parseScreenText: this.getParseScreenText(screenText),
|
|
1854
|
+
workingDir: this.workingDir,
|
|
1855
|
+
providerSessionId: this.providerSessionId,
|
|
1856
|
+
runtimeSettings: this.runtimeSettings,
|
|
1857
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1858
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1859
|
+
lastOutputAt: this.lastOutputAt,
|
|
1860
|
+
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
1861
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1862
|
+
lastScreenSnapshot: this.lastScreenSnapshot,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
isAlive(): boolean { return this.ptyProcess !== null; }
|
|
1866
|
+
flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
|
|
1867
|
+
|
|
1868
|
+
async writeRaw(data: string | Buffer): Promise<void> {
|
|
1869
|
+
const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
|
1870
|
+
await this.writeToPty(str);
|
|
2298
1871
|
}
|
|
2299
1872
|
|
|
2300
1873
|
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
|
-
}
|
|
1874
|
+
this.engine.resolveModal(buttonIndex);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
1878
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
1882
|
+
isApprovalRecentlyResolved(): boolean {
|
|
1883
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2347
1884
|
}
|
|
2348
1885
|
|
|
2349
1886
|
resize(cols: number, rows: number): void {
|
|
@@ -2357,7 +1894,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2357
1894
|
}
|
|
2358
1895
|
|
|
2359
1896
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2360
|
-
if (this.startupParseGate ||
|
|
1897
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2361
1898
|
try {
|
|
2362
1899
|
const parsed = this.getScriptParsedStatus();
|
|
2363
1900
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2369,18 +1906,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2369
1906
|
getDebugState(): Record<string, any> {
|
|
2370
1907
|
const screenText = sanitizeTerminalText(this.terminalScreen.getText());
|
|
2371
1908
|
const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
2372
|
-
const
|
|
2373
|
-
|
|
1909
|
+
const startupDetectedStatus = this.startupParseGate && !startupModal
|
|
1910
|
+
? this.runDetectStatus(this.recentOutputBuffer || screenText)
|
|
1911
|
+
: null;
|
|
1912
|
+
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2374
1913
|
const parsedDebugState = this.getParsedDebugState();
|
|
2375
1914
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
1915
|
+
const hasFinalAssistant = (p: any) => {
|
|
1916
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1917
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1918
|
+
};
|
|
1919
|
+
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1920
|
+
if (parsedDebugState?.status === 'error') {
|
|
1921
|
+
effectiveStatus = 'error';
|
|
1922
|
+
}
|
|
1923
|
+
// (fix) Mirror the getStatus contract: never surface waiting_approval
|
|
1924
|
+
// without a concrete modal. detectStatus alone can fire while
|
|
1925
|
+
// parseApproval is still null, and a bare debugState waiting_approval
|
|
1926
|
+
// confused dashboards reading the debug payload.
|
|
1927
|
+
const debugEffectiveModal = startupModal || this.engine.activeModal;
|
|
1928
|
+
if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
|
|
1929
|
+
effectiveStatus = 'waiting_approval';
|
|
1930
|
+
}
|
|
1931
|
+
if (
|
|
1932
|
+
effectiveStatus === 'idle'
|
|
1933
|
+
&& parsedDebugState?.status === 'generating'
|
|
1934
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
1935
|
+
) {
|
|
1936
|
+
effectiveStatus = 'generating';
|
|
1937
|
+
}
|
|
2376
1938
|
return {
|
|
2377
1939
|
type: this.cliType,
|
|
2378
1940
|
name: this.cliName,
|
|
2379
1941
|
providerResolution: this.providerResolutionMeta,
|
|
2380
1942
|
status: effectiveStatus,
|
|
2381
1943
|
projectedStatus: effectiveStatus,
|
|
2382
|
-
rawStatus: this.currentStatus,
|
|
2383
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
1944
|
+
rawStatus: this.engine.currentStatus,
|
|
1945
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2384
1946
|
ready: effectiveReady,
|
|
2385
1947
|
startupParseGate: this.startupParseGate,
|
|
2386
1948
|
spawnAt: this.spawnAt,
|
|
@@ -2394,38 +1956,47 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2394
1956
|
providerSessionId: parsedDebugState.providerSessionId,
|
|
2395
1957
|
transcriptAuthority: parsedDebugState.transcriptAuthority,
|
|
2396
1958
|
coverage: parsedDebugState.coverage,
|
|
1959
|
+
errorMessage: parsedDebugState.errorMessage,
|
|
1960
|
+
errorReason: parsedDebugState.errorReason,
|
|
2397
1961
|
activeModal: parsedDebugState.activeModal,
|
|
2398
1962
|
messageCount: parsedMessages.length,
|
|
2399
1963
|
} : null,
|
|
2400
1964
|
screenText: screenText.slice(-4000),
|
|
2401
|
-
currentTurnScope: this.currentTurnScope,
|
|
1965
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2402
1966
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2403
1967
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2404
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2405
1968
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2406
1969
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2407
1970
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
2408
1971
|
sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
|
|
2409
1972
|
responseBuffer: this.responseBuffer.slice(-1000),
|
|
1973
|
+
pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
|
|
1974
|
+
id: message.id,
|
|
1975
|
+
role: message.role,
|
|
1976
|
+
content: message.content,
|
|
1977
|
+
queuedAt: message.queuedAt,
|
|
1978
|
+
source: message.source,
|
|
1979
|
+
})),
|
|
1980
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
2410
1981
|
lastOutputAt: this.lastOutputAt,
|
|
2411
1982
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2412
1983
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2413
1984
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2414
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2415
|
-
activeModal: startupModal || this.activeModal,
|
|
2416
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1985
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1986
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
1987
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2417
1988
|
sendDelayMs: this.sendDelayMs,
|
|
2418
1989
|
sendKey: this.sendKey,
|
|
2419
1990
|
submitStrategy: this.submitStrategy,
|
|
2420
1991
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2421
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2422
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1992
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1993
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2423
1994
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2424
1995
|
hasCliScripts: this.hasCliScripts(),
|
|
2425
|
-
scriptNames:
|
|
2426
|
-
traceSessionId: this.
|
|
2427
|
-
traceEntryCount: this.
|
|
2428
|
-
statusHistory: this.
|
|
1996
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1997
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1998
|
+
traceEntryCount: this.engine.getTraceEntries().length,
|
|
1999
|
+
statusHistory: this.engine.getStatusHistory().slice(-30),
|
|
2429
2000
|
timeouts: this.timeouts,
|
|
2430
2001
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2431
2002
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2435,20 +2006,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2435
2006
|
|
|
2436
2007
|
getTraceState(limit = 120): Record<string, any> {
|
|
2437
2008
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
2009
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2438
2010
|
return {
|
|
2439
|
-
sessionId: this.
|
|
2011
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2440
2012
|
providerResolution: this.providerResolutionMeta,
|
|
2441
|
-
entryCount:
|
|
2442
|
-
entries:
|
|
2013
|
+
entryCount: traceEntries.length,
|
|
2014
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2443
2015
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2444
2016
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2445
2017
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2446
2018
|
status: this.projectEffectiveStatus(),
|
|
2447
2019
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2448
|
-
rawStatus: this.currentStatus,
|
|
2449
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2450
|
-
activeModal: this.activeModal,
|
|
2451
|
-
currentTurnScope: this.currentTurnScope,
|
|
2020
|
+
rawStatus: this.engine.currentStatus,
|
|
2021
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2022
|
+
activeModal: this.engine.activeModal,
|
|
2023
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2452
2024
|
messages: [],
|
|
2453
2025
|
};
|
|
2454
2026
|
}
|