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