@adhdev/daemon-core 0.9.82-rc.36 → 0.9.82-rc.361
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 +55 -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 +116 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +53 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +14 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/cli-manager.d.ts +15 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/types.d.ts +16 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +336 -2
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +77 -1
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +11 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +23 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/index.d.ts +2 -0
- package/dist/index.d.ts +54 -13
- package/dist/index.js +41337 -14723
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +44100 -17614
- 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/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +36 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +174 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +170 -0
- package/dist/mesh/mesh-events-pending.d.ts +55 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +60 -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 +164 -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 +429 -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 +246 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +216 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +20 -0
- package/dist/providers/chat-message-normalization.d.ts +12 -0
- package/dist/providers/cli-provider-instance.d.ts +106 -3
- package/dist/providers/contracts.d.ts +139 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +63 -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 +14 -0
- package/dist/providers/provider-instance.d.ts +23 -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 +86 -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 +91 -0
- package/dist/providers/spec/cli-adapter.d.ts +223 -0
- package/dist/providers/spec/evaluator.d.ts +45 -0
- package/dist/providers/spec/fsm-driver.d.ts +368 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +76 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +174 -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 +222 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +295 -13
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +59 -2
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +26 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +55 -12
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +56 -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 +774 -1141
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +100 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +90 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
- package/src/commands/chat-commands.ts +1823 -62
- package/src/commands/cli-manager.ts +405 -23
- package/src/commands/handler.ts +841 -2
- package/src/commands/low-family/index.ts +23 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +274 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/types.ts +19 -0
- package/src/commands/mesh-coordinator.ts +332 -122
- package/src/commands/router.ts +7195 -1423
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +483 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +388 -6
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/cli-detector.ts +28 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +55 -14
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +574 -43
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +8 -1
- package/src/git/index.ts +16 -0
- package/src/index.ts +156 -12
- 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/command-log.ts +7 -5
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +220 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +338 -0
- package/src/mesh/coordinator-prompt.ts +234 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +667 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +2572 -0
- package/src/mesh/mesh-events-pending.ts +599 -0
- package/src/mesh/mesh-events-stale.ts +309 -0
- package/src/mesh/mesh-events-utils.ts +361 -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 +591 -102
- package/src/mesh/mesh-missions.ts +322 -0
- package/src/mesh/mesh-reconcile-loop.ts +1096 -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 +1697 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/mesh/mesh-work-queue.ts +997 -141
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +423 -0
- package/src/mesh/worktree-bootstrap-config.ts +331 -0
- package/src/providers/acp-provider-instance.ts +43 -10
- package/src/providers/approval-utils.d.ts +5 -0
- package/src/providers/approval-utils.ts +57 -5
- package/src/providers/chat-message-normalization.ts +45 -5
- package/src/providers/cli-provider-instance.ts +1267 -101
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +150 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +85 -0
- 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 +19 -1
- package/src/providers/provider-loader.ts +670 -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 +313 -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 +235 -0
- package/src/providers/spec/cli-adapter.ts +1051 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1251 -0
- package/src/providers/spec/fsm-evaluator.ts +272 -0
- package/src/providers/spec/fsm-loader.ts +120 -0
- package/src/providers/spec/fsm-types.ts +273 -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 +262 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +378 -14
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +64 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +95 -26
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- 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 '';
|
|
@@ -119,26 +118,52 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
|
|
|
119
118
|
return current.slice(-keepFromCurrent) + chunk;
|
|
120
119
|
}
|
|
121
120
|
|
|
121
|
+
// Force-send (mesh coordinator dispatch / reconcile redelivery) writes raw
|
|
122
|
+
// keystrokes straight into the PTY, bypassing the normal echo-wait submit
|
|
123
|
+
// pipeline.
|
|
124
|
+
//
|
|
125
|
+
// History of this path's failure modes:
|
|
126
|
+
// 1. Originally it wrote `text + sendKey` in a single PTY write with zero
|
|
127
|
+
// settle time. Injected into an idle TUI input box that was still entering
|
|
128
|
+
// its input-accepting state, the trailing submit key could be swallowed —
|
|
129
|
+
// prompt typed but never submitted ("text injected, Enter not pressed").
|
|
130
|
+
// 2. The first fix split inject from submit: write(text) → echo-gated settle →
|
|
131
|
+
// write(sendKey) as a *separate* PTY write. That broke win32: ConPTY/TUI
|
|
132
|
+
// does not recognize a lone '\r' that arrives in its own PTY chunk
|
|
133
|
+
// 150ms+ after the text as a submit key, and the win32 echo gate never
|
|
134
|
+
// matched (ConPTY echo/parsing differences), so the cap was burned and the
|
|
135
|
+
// same detached lone CR was emitted — adding latency while the Enter still
|
|
136
|
+
// got swallowed.
|
|
137
|
+
//
|
|
138
|
+
// Current behavior: keep a fixed settle gap so the input handler is in its
|
|
139
|
+
// input-accepting state, then write `content + sendKey` as a SINGLE atomic PTY
|
|
140
|
+
// write — the same verified mechanism the normal `immediate` submit strategy
|
|
141
|
+
// uses (submitImmediatePrompt). With the Enter in the same write unit as the
|
|
142
|
+
// text, win32 ConPTY always sees it as a submit and the race that the split was
|
|
143
|
+
// trying to solve cannot happen (the Enter can never be separated from the
|
|
144
|
+
// text). FORCE_SUBMIT_SETTLE_MS is that minimum pre-submit gap.
|
|
145
|
+
const FORCE_SUBMIT_SETTLE_MS = 150;
|
|
146
|
+
|
|
122
147
|
// ─── Adapter ────────────────────────────────────────
|
|
123
148
|
|
|
124
149
|
export class ProviderCliAdapter implements CliAdapter {
|
|
125
|
-
|
|
150
|
+
cliType: string;
|
|
126
151
|
readonly cliName: string;
|
|
127
152
|
public workingDir: string;
|
|
128
153
|
|
|
129
154
|
private provider: CliProviderModule;
|
|
130
155
|
private ptyProcess: PtyRuntimeTransport | null = null;
|
|
131
156
|
private transportFactory: PtyTransportFactory;
|
|
132
|
-
private currentStatus: CliSessionStatus['status'] = 'starting';
|
|
133
157
|
private onStatusChange: (() => void) | null = null;
|
|
134
158
|
|
|
159
|
+
// ─── State machine engine ─────────────────────────
|
|
160
|
+
readonly engine: CliStateEngine;
|
|
161
|
+
|
|
135
162
|
private responseBuffer = '';
|
|
136
163
|
private recentOutputBuffer = '';
|
|
137
|
-
private
|
|
138
|
-
private
|
|
139
|
-
private parseErrorMessage: string | null = null;
|
|
164
|
+
private get parseErrorMessage(): string | null { return this.runner.parseErrorMessage; }
|
|
165
|
+
private providerSessionId: string | null = null;
|
|
140
166
|
private responseTimeout: NodeJS.Timeout | null = null;
|
|
141
|
-
private idleTimeout: NodeJS.Timeout | null = null;
|
|
142
167
|
private ready = false;
|
|
143
168
|
private startupBuffer = '';
|
|
144
169
|
private startupParseGate = false;
|
|
@@ -164,39 +189,48 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
164
189
|
private serverConn: any = null;
|
|
165
190
|
private logBuffer: { message: string; level: string }[] = [];
|
|
166
191
|
|
|
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;
|
|
192
|
+
private pendingOutboundQueue: PendingOutboundMessage[] = [];
|
|
193
|
+
private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
|
|
194
|
+
private pendingOutboundFlushInFlight = false;
|
|
195
|
+
// Submit retry timer — PTY-level, not state machine
|
|
183
196
|
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
197
|
|
|
190
198
|
// Resize redraw suppression
|
|
191
199
|
private resizeSuppressUntil: number = 0;
|
|
192
200
|
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
// (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
|
|
202
|
+
// ChatSourceMachine holds the lock by state, not by a mutable field on
|
|
203
|
+
// the adapter. Removed entirely; no callers remain after the readChat
|
|
204
|
+
// ladder was replaced.
|
|
205
|
+
|
|
206
|
+
// ─── Script runner (parsing isolated here, adapter stays as transport) ───
|
|
207
|
+
private readonly runner: CliScriptRunner;
|
|
208
|
+
/** @deprecated use runner.cliScripts for direct script access */
|
|
209
|
+
get cliScripts(): CliScripts { return this.runner.cliScripts; }
|
|
195
210
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Recent script invocations (oldest → newest). Exposed via the
|
|
213
|
+
* `get_chat_debug_bundle` daemon command so anyone debugging a
|
|
214
|
+
* stuck-status regression can read what each detectStatus /
|
|
215
|
+
* parseSession call actually saw and returned, instead of having
|
|
216
|
+
* to instrument the daemon.
|
|
217
|
+
*/
|
|
218
|
+
getScriptInvocationTrace() {
|
|
219
|
+
return this.runner.getInvocationTrace();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Returns the full raw PTY byte stream captured since adapter start.
|
|
224
|
+
* Used by the `record_provider_pty` IPC command to produce fixtures.
|
|
225
|
+
* Bounded by MAX_ACCUMULATED_BUFFER; older bytes may have been dropped.
|
|
226
|
+
*/
|
|
227
|
+
getAccumulatedRawBuffer(): { text: string; droppedChars: number } {
|
|
228
|
+
return {
|
|
229
|
+
text: this.accumulatedRawBuffer,
|
|
230
|
+
droppedChars: this.accumulatedRawBufferDroppedChars,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
|
|
200
234
|
private runtimeSettings: Record<string, any> = {};
|
|
201
235
|
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
202
236
|
private accumulatedBuffer: string = '';
|
|
@@ -205,7 +239,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
205
239
|
/** Full accumulated raw PTY output (with ANSI) */
|
|
206
240
|
private accumulatedRawBuffer: string = '';
|
|
207
241
|
/** Current visible terminal screen snapshot */
|
|
208
|
-
private terminalScreen = new TerminalScreen(
|
|
242
|
+
private terminalScreen = new TerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
|
|
209
243
|
private static readonly MAX_RESPONSE_BUFFER = 8000;
|
|
210
244
|
private static readonly MAX_RECENT_OUTPUT_BUFFER = 1000;
|
|
211
245
|
private responseBufferDroppedChars = 0;
|
|
@@ -216,10 +250,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
216
250
|
* Hermes turn (tool calls + reasoning + final bubble) without the
|
|
217
251
|
* rolling window pushing the turn's ╭─ opening line out of view. */
|
|
218
252
|
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
253
|
private parsedStatusCache: {
|
|
224
254
|
responseBuffer: string;
|
|
225
255
|
currentTurnScope: TurnParseScope | null;
|
|
@@ -233,11 +263,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
233
263
|
result: any;
|
|
234
264
|
} | null = null;
|
|
235
265
|
private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
236
|
-
private static readonly MAX_TRACE_ENTRIES = 250;
|
|
237
266
|
|
|
238
267
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
239
|
-
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
240
|
-
private static readonly MAX_FINISH_RETRIES = 2;
|
|
241
268
|
|
|
242
269
|
private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
|
|
243
270
|
const build = (droppedChars: number, maxChars: number) => droppedChars > 0
|
|
@@ -313,13 +340,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
313
340
|
if (
|
|
314
341
|
cached
|
|
315
342
|
&& cached.responseBuffer === this.responseBuffer
|
|
316
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
343
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
317
344
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
318
345
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
319
346
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
320
347
|
&& cached.screenText === this.lastScreenText
|
|
321
|
-
&& cached.currentStatus === this.currentStatus
|
|
322
|
-
&& cached.activeModal === this.activeModal
|
|
348
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
349
|
+
&& cached.activeModal === this.engine.activeModal
|
|
323
350
|
&& cached.cliName === this.cliName
|
|
324
351
|
) {
|
|
325
352
|
return cached.result;
|
|
@@ -343,86 +370,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
343
370
|
return this.timeouts.statusActivityHold;
|
|
344
371
|
}
|
|
345
372
|
|
|
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
373
|
// Resolved timeouts
|
|
427
374
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
428
375
|
|
|
@@ -432,7 +379,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
432
379
|
private readonly sendKey: string;
|
|
433
380
|
private readonly submitStrategy: 'wait_for_echo' | 'immediate';
|
|
434
381
|
private readonly requirePromptEchoBeforeSubmit: boolean;
|
|
435
|
-
private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
|
|
436
382
|
|
|
437
383
|
constructor(
|
|
438
384
|
provider: CliProviderModule,
|
|
@@ -441,6 +387,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
441
387
|
private extraEnv: Record<string, string> = {},
|
|
442
388
|
transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
|
|
443
389
|
) {
|
|
390
|
+
this.runner = new CliScriptRunner(provider.type);
|
|
444
391
|
this.provider = provider;
|
|
445
392
|
this.transportFactory = transportFactory;
|
|
446
393
|
this.cliType = provider.type;
|
|
@@ -458,10 +405,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
458
405
|
this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
|
|
459
406
|
this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
|
|
460
407
|
|
|
461
|
-
//
|
|
462
|
-
this.
|
|
463
|
-
|
|
464
|
-
|
|
408
|
+
// State machine engine — owns all status transitions
|
|
409
|
+
this.engine = new CliStateEngine(
|
|
410
|
+
provider,
|
|
411
|
+
this.runner,
|
|
412
|
+
this as unknown as CliTransportAccess,
|
|
413
|
+
{
|
|
414
|
+
onStatusChange: () => { this.onStatusChange?.(); },
|
|
415
|
+
onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
|
|
416
|
+
onTurnCompleted: () => { this.responseBuffer = ''; },
|
|
417
|
+
} satisfies CliStateEngineCallbacks,
|
|
418
|
+
resolvedConfig.timeouts,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// Scripts delegated to CliScriptRunner — adapter stays as transport.
|
|
422
|
+
// Pass the manifest tui block so the runner can build the
|
|
423
|
+
// declarativeDetectStatus / declarativeParseApproval SDK functions
|
|
424
|
+
// that v1 extended-tier overrides depend on (without these, scripts
|
|
425
|
+
// like codex-cli's detect_status v1 fail-closed with return 'idle'
|
|
426
|
+
// and the session sticks in `generating` forever).
|
|
427
|
+
this.runner.setScripts(provider.scripts || {}, provider.tui);
|
|
428
|
+
// Per-invocation wall-clock budget for provider scripts. Manifests may
|
|
429
|
+
// raise this for genuinely slow parsers, but the default (50ms) is the
|
|
430
|
+
// settle-loop safety net — any script that exceeds it gets flagged in
|
|
431
|
+
// the invocation trace and surfaced via the debug bundle so we can
|
|
432
|
+
// identify the offender instead of guessing why the settle loop hangs.
|
|
433
|
+
this.runner.setScriptCallBudget(provider.scriptCallBudgetMs ?? 50);
|
|
434
|
+
const scriptNames = this.runner.getScriptNames();
|
|
465
435
|
if (scriptNames.length > 0) {
|
|
466
436
|
LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
|
|
467
437
|
LOG.info(
|
|
@@ -487,14 +457,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
487
457
|
|
|
488
458
|
/** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
|
|
489
459
|
setCliScripts(scripts: CliScripts): void {
|
|
490
|
-
this.
|
|
460
|
+
this.runner.setScripts(scripts);
|
|
491
461
|
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(', ')}]`);
|
|
462
|
+
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
|
|
498
463
|
}
|
|
499
464
|
|
|
500
465
|
/** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
|
|
@@ -548,15 +513,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
548
513
|
});
|
|
549
514
|
|
|
550
515
|
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
516
|
|
|
561
517
|
try {
|
|
562
518
|
this.ptyProcess = this.transportFactory.spawn(
|
|
@@ -620,13 +576,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
620
576
|
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
621
577
|
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
|
|
622
578
|
this.flushPendingOutputParse();
|
|
623
|
-
this.recordTrace('exit', { exitCode });
|
|
624
579
|
this.ptyProcess = null;
|
|
625
|
-
this.
|
|
580
|
+
this.engine.onPtyExit();
|
|
626
581
|
this.ready = false;
|
|
627
582
|
this.startupParseGate = false;
|
|
628
583
|
this.spawnAt = 0;
|
|
629
|
-
this.
|
|
584
|
+
this.runner.resetSessionState();
|
|
630
585
|
this.onStatusChange?.();
|
|
631
586
|
});
|
|
632
587
|
|
|
@@ -635,17 +590,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
635
590
|
this.startupBuffer = '';
|
|
636
591
|
this.startupFirstOutputAt = 0;
|
|
637
592
|
if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
|
|
638
|
-
this.resetTerminalScreen(
|
|
593
|
+
this.resetTerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
|
|
639
594
|
this.pendingTerminalQueryTail = '';
|
|
640
|
-
this.currentTurnScope = null;
|
|
641
|
-
this.finishRetryCount = 0;
|
|
642
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
643
595
|
this.ready = false;
|
|
644
596
|
await this.ptyProcess.ready;
|
|
645
|
-
this.
|
|
646
|
-
runtimeMeta: this.getRuntimeMetadata(),
|
|
647
|
-
});
|
|
648
|
-
this.setStatus('starting', 'pty_ready');
|
|
597
|
+
this.engine.onSpawnReady();
|
|
649
598
|
this.scheduleStartupSettleCheck();
|
|
650
599
|
this.onStatusChange?.();
|
|
651
600
|
}
|
|
@@ -671,11 +620,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
671
620
|
if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
|
|
672
621
|
this.startupFirstOutputAt = now;
|
|
673
622
|
}
|
|
674
|
-
if (
|
|
675
|
-
this.clearIdleFinishCandidate('new_output');
|
|
623
|
+
if (rawData.length > 0 || cleanData.length > 0) {
|
|
624
|
+
this.engine.clearIdleFinishCandidate('new_output');
|
|
676
625
|
}
|
|
677
626
|
if (getDebugRuntimeConfig().collectDebugTrace) {
|
|
678
|
-
this.
|
|
627
|
+
this.engine.recordExternalTrace('output', {
|
|
679
628
|
rawLength: rawData.length,
|
|
680
629
|
cleanLength: cleanData.length,
|
|
681
630
|
rawPreview: summarizeCliTraceText(rawData, 300),
|
|
@@ -687,7 +636,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
687
636
|
this.scheduleStartupSettleCheck();
|
|
688
637
|
}
|
|
689
638
|
|
|
690
|
-
if (this.isWaitingForResponse && cleanData) {
|
|
639
|
+
if (this.engine.isWaitingForResponse && cleanData) {
|
|
691
640
|
const previousResponseLen = this.responseBuffer.length;
|
|
692
641
|
this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
|
|
693
642
|
this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
|
|
@@ -726,19 +675,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
726
675
|
// Keep turn-scope offsets aligned with the truncated buffer so scoped
|
|
727
676
|
// parses don't lose the beginning of a long turn (e.g. the Hermes
|
|
728
677
|
// ╭─ opening line) when the rolling window sheds bytes.
|
|
729
|
-
if (this.currentTurnScope) {
|
|
678
|
+
if (this.engine.currentTurnScope) {
|
|
730
679
|
if (droppedClean > 0) {
|
|
731
|
-
this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
|
|
680
|
+
this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
|
|
732
681
|
}
|
|
733
682
|
if (droppedRaw > 0) {
|
|
734
|
-
this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
|
|
683
|
+
this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
|
|
735
684
|
}
|
|
736
685
|
}
|
|
737
686
|
|
|
738
687
|
this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
|
|
739
688
|
|
|
740
689
|
// ─── Script-based status detection
|
|
741
|
-
this.scheduleSettle();
|
|
690
|
+
this.engine.scheduleSettle();
|
|
742
691
|
}
|
|
743
692
|
|
|
744
693
|
private resolveStartupState(
|
|
@@ -761,6 +710,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
761
710
|
if (stableMs < 2000) return;
|
|
762
711
|
|
|
763
712
|
const startupModal = this.runParseApproval(this.recentOutputBuffer);
|
|
713
|
+
const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
714
|
+
if (!startupModal && startupStatus !== 'idle') {
|
|
715
|
+
this.scheduleStartupSettleCheck();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
764
718
|
this.startupParseGate = false;
|
|
765
719
|
if (this.startupSettleTimer) {
|
|
766
720
|
clearTimeout(this.startupSettleTimer);
|
|
@@ -768,14 +722,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
768
722
|
}
|
|
769
723
|
this.ready = true;
|
|
770
724
|
if (startupModal) {
|
|
771
|
-
this.activeModal = startupModal;
|
|
772
|
-
this.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
725
|
+
this.engine.activeModal = startupModal;
|
|
726
|
+
this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
773
727
|
} else {
|
|
774
|
-
if (this.currentStatus === 'waiting_approval' || this.activeModal) {
|
|
775
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
728
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
|
|
729
|
+
this.engine.lastApprovalResolvedAt = Date.now();
|
|
776
730
|
}
|
|
777
|
-
this.activeModal = null;
|
|
778
|
-
|
|
731
|
+
this.engine.activeModal = null;
|
|
732
|
+
// Clear the in-flight turn flag at the same time we declare
|
|
733
|
+
// startup-idle. Otherwise the next settled evaluation sees
|
|
734
|
+
// isWaitingForResponse=true + recent CLI welcome-screen paints
|
|
735
|
+
// and flips us right back to generating via the hold path
|
|
736
|
+
// (the "startup → generating → idle → generating → idle"
|
|
737
|
+
// flicker the user observed on claude-cli launch).
|
|
738
|
+
this.engine.isWaitingForResponse = false;
|
|
739
|
+
this.engine.currentTurnScope = null;
|
|
740
|
+
this.engine.setStatus('idle', `startup_ready:${trigger}`);
|
|
779
741
|
}
|
|
780
742
|
LOG.info(
|
|
781
743
|
'CLI',
|
|
@@ -801,88 +763,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
801
763
|
}, delayMs);
|
|
802
764
|
}
|
|
803
765
|
|
|
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
766
|
private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
|
|
887
767
|
const startedAt = Date.now();
|
|
888
768
|
let loggedWait = false;
|
|
@@ -892,7 +772,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
892
772
|
const screenText = this.terminalScreen.getText() || '';
|
|
893
773
|
const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
|
|
894
774
|
const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
895
|
-
const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
775
|
+
const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
|
|
896
776
|
const interactiveReady = status === 'idle'
|
|
897
777
|
&& stableMs >= 700
|
|
898
778
|
&& recentlyOutput >= 350;
|
|
@@ -926,637 +806,84 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
926
806
|
|
|
927
807
|
private clearAllTimers(): void {
|
|
928
808
|
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
809
|
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
810
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
936
811
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
812
|
+
this.engine.clearAllTimers();
|
|
937
813
|
}
|
|
938
814
|
|
|
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
|
-
}
|
|
815
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
996
816
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const modal = (session as any).activeModal ?? session.modal ?? null;
|
|
1016
|
-
const parsedMessages = normalizeCliParsedMessages(messages, {
|
|
1017
|
-
scope: null,
|
|
1018
|
-
lastOutputAt: this.lastOutputAt,
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages)) return;
|
|
1022
|
-
|
|
1023
|
-
const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
|
|
1024
|
-
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || '');
|
|
1025
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1026
|
-
|
|
1027
|
-
this.recordTrace('settled', {
|
|
1028
|
-
tail: summarizeCliTraceText(this.settledBuffer, 500),
|
|
1029
|
-
screenText: summarizeCliTraceText(screenText, 1200),
|
|
1030
|
-
detectStatus: status,
|
|
1031
|
-
parsedStatus: parsedStatus || null,
|
|
1032
|
-
parsedMessageCount: parsedMessages.length,
|
|
1033
|
-
parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
|
|
1034
|
-
parsedActiveModal: modal,
|
|
1035
|
-
approval: modal,
|
|
1036
|
-
...buildCliTraceParseSnapshot({
|
|
1037
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1038
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1039
|
-
responseBuffer: this.responseBuffer,
|
|
1040
|
-
partialResponse: this.responseBuffer,
|
|
1041
|
-
scope: this.currentTurnScope,
|
|
1042
|
-
}),
|
|
817
|
+
runParseSession(): ParsedSession | null {
|
|
818
|
+
const screenText = this.terminalScreen.getText();
|
|
819
|
+
const parseScreenText = this.getParseScreenText(screenText);
|
|
820
|
+
const tail = this.recentOutputBuffer.slice(-500);
|
|
821
|
+
const input = buildCliParseInput({
|
|
822
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
823
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
824
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
825
|
+
terminalScreenText: parseScreenText,
|
|
826
|
+
workingDir: this.workingDir,
|
|
827
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
828
|
+
historySessionId: this.providerSessionId || undefined,
|
|
829
|
+
baseMessages: [],
|
|
830
|
+
partialResponse: this.responseBuffer,
|
|
831
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
832
|
+
scope: this.engine.currentTurnScope,
|
|
833
|
+
runtimeSettings: this.runtimeSettings,
|
|
834
|
+
spawnAt: this.spawnAt,
|
|
1043
835
|
});
|
|
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
|
-
}),
|
|
836
|
+
const session = this.runner.parseSession({
|
|
837
|
+
...input,
|
|
838
|
+
tail,
|
|
839
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1168
840
|
});
|
|
1169
|
-
this.
|
|
841
|
+
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
842
|
+
return session;
|
|
1170
843
|
}
|
|
1171
844
|
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
this.
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
this.idleTimeout = setTimeout(() => {
|
|
1183
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1184
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1185
|
-
this.finishResponse();
|
|
1186
|
-
}
|
|
1187
|
-
}, this.timeouts.generatingIdle);
|
|
1188
|
-
} else {
|
|
1189
|
-
this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
|
|
1190
|
-
}
|
|
1191
|
-
this.onStatusChange?.();
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
if (!inCooldown) {
|
|
1195
|
-
if (!modal) {
|
|
1196
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
this.isWaitingForResponse = true;
|
|
1200
|
-
this.setStatus('waiting_approval', 'script_detect');
|
|
1201
|
-
this.activeModal = modal;
|
|
1202
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1203
|
-
this.armApprovalExitTimeout();
|
|
1204
|
-
this.onStatusChange?.();
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
private applyGenerating(ctx: SettledEvalContext): void {
|
|
1209
|
-
const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
|
|
1210
|
-
this.clearIdleFinishCandidate('generating');
|
|
1211
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1212
|
-
const effectiveScreenText = screenText || this.accumulatedBuffer;
|
|
1213
|
-
const noActiveTurn = !this.currentTurnScope;
|
|
1214
|
-
const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
|
|
1215
|
-
const parsedShowsLiveAssistantProgress = parsedStatus === 'generating'
|
|
1216
|
-
&& !!lastParsedAssistant
|
|
1217
|
-
;
|
|
1218
|
-
if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (prevStatus === 'waiting_approval') {
|
|
1222
|
-
// Transitioned out of approval → generating
|
|
1223
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1224
|
-
this.activeModal = null;
|
|
1225
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1226
|
-
}
|
|
1227
|
-
if (!this.isWaitingForResponse) {
|
|
1228
|
-
this.isWaitingForResponse = true;
|
|
1229
|
-
this.responseBuffer = '';
|
|
1230
|
-
}
|
|
1231
|
-
this.setStatus('generating', 'script_detect');
|
|
1232
|
-
// Reset idle timeout
|
|
1233
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1234
|
-
this.idleTimeout = setTimeout(() => {
|
|
1235
|
-
if (this.isWaitingForResponse) {
|
|
1236
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1237
|
-
this.finishResponse();
|
|
1238
|
-
}
|
|
1239
|
-
}, this.timeouts.generatingIdle);
|
|
1240
|
-
this.onStatusChange?.();
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
private applyIdle(ctx: SettledEvalContext, now: number): void {
|
|
1244
|
-
const { modal, lastParsedAssistant, prevStatus } = ctx;
|
|
1245
|
-
if (prevStatus === 'waiting_approval') {
|
|
1246
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1247
|
-
this.activeModal = null;
|
|
1248
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1249
|
-
this.setStatus('idle', 'approval_prompt_gone_script_idle');
|
|
1250
|
-
}
|
|
1251
|
-
if (!this.isWaitingForResponse) {
|
|
1252
|
-
if (prevStatus !== 'idle') {
|
|
1253
|
-
this.clearIdleFinishCandidate('idle_without_response');
|
|
1254
|
-
this.setStatus('idle', 'script_detect');
|
|
1255
|
-
this.onStatusChange?.();
|
|
1256
|
-
}
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
1260
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
1261
|
-
const hasAssistantTurn = !!lastParsedAssistant;
|
|
1262
|
-
const assistantLength = lastParsedAssistant?.content?.length || 0;
|
|
1263
|
-
const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
|
|
1264
|
-
const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
|
|
1265
|
-
const idleReady = !modal
|
|
1266
|
-
&& hasAssistantTurn
|
|
1267
|
-
&& quietForMs >= idleQuietThresholdMs
|
|
1268
|
-
&& screenStableMs >= idleFinishConfirmMs;
|
|
1269
|
-
const candidate = this.idleFinishCandidate;
|
|
1270
|
-
const candidateQuiet = !!candidate
|
|
1271
|
-
&& candidate.responseEpoch === this.responseEpoch
|
|
1272
|
-
&& candidate.lastOutputAt === this.lastOutputAt
|
|
1273
|
-
&& candidate.lastScreenChangeAt === this.lastScreenChangeAt
|
|
1274
|
-
&& assistantLength >= candidate.assistantLength
|
|
1275
|
-
&& (now - candidate.armedAt) >= idleFinishConfirmMs;
|
|
1276
|
-
|
|
1277
|
-
this.recordTrace('idle_decision', {
|
|
1278
|
-
quietForMs,
|
|
1279
|
-
screenStableMs,
|
|
1280
|
-
hasAssistantTurn,
|
|
1281
|
-
assistantLength,
|
|
1282
|
-
hasModal: !!modal,
|
|
1283
|
-
idleQuietThresholdMs,
|
|
1284
|
-
idleStableThresholdMs: idleFinishConfirmMs,
|
|
1285
|
-
idleReady,
|
|
1286
|
-
idleFinishConfirmMs,
|
|
1287
|
-
idleFinishCandidate: candidate,
|
|
1288
|
-
candidateQuiet,
|
|
1289
|
-
canFinishImmediately: idleReady && candidateQuiet,
|
|
1290
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
1291
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1292
|
-
...buildCliTraceParseSnapshot({
|
|
1293
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1294
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1295
|
-
responseBuffer: this.responseBuffer,
|
|
1296
|
-
partialResponse: this.responseBuffer,
|
|
1297
|
-
scope: this.currentTurnScope,
|
|
1298
|
-
}),
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
if (idleReady && candidateQuiet) {
|
|
1302
|
-
this.clearIdleFinishCandidate('finish_response');
|
|
1303
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1304
|
-
this.finishResponse();
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (idleReady) {
|
|
1309
|
-
if (!candidate) {
|
|
1310
|
-
this.armIdleFinishCandidate(assistantLength);
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
} else {
|
|
1314
|
-
this.clearIdleFinishCandidate('idle_not_ready');
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1318
|
-
this.idleTimeout = setTimeout(() => {
|
|
1319
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1320
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1321
|
-
this.clearIdleFinishCandidate('idle_timeout_finish');
|
|
1322
|
-
this.finishResponse();
|
|
1323
|
-
}
|
|
1324
|
-
}, this.timeouts.idleFinish);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
private finishResponse(): void {
|
|
1328
|
-
if (this.submitPendingUntil > Date.now()) return;
|
|
1329
|
-
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
1330
|
-
this.clearIdleFinishCandidate('finish_response_enter');
|
|
1331
|
-
this.recordTrace('finish_response', {
|
|
1332
|
-
...buildCliTraceParseSnapshot({
|
|
1333
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1334
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1335
|
-
responseBuffer: this.responseBuffer,
|
|
1336
|
-
partialResponse: this.responseBuffer,
|
|
1337
|
-
scope: this.currentTurnScope,
|
|
1338
|
-
}),
|
|
845
|
+
runDetectStatus(text: string): string | null {
|
|
846
|
+
const screenText = this.terminalScreen.getText();
|
|
847
|
+
const tail = text.slice(-500);
|
|
848
|
+
return this.runner.detectStatus({
|
|
849
|
+
tail,
|
|
850
|
+
screenText,
|
|
851
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
852
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
853
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
854
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1339
855
|
});
|
|
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
856
|
}
|
|
1376
857
|
|
|
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),
|
|
858
|
+
runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
859
|
+
const screenText = this.terminalScreen.getText();
|
|
860
|
+
const buffer = screenText || this.accumulatedBuffer;
|
|
861
|
+
return this.runner.parseApproval({
|
|
862
|
+
buffer,
|
|
863
|
+
screenText,
|
|
864
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
865
|
+
tail,
|
|
866
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
867
|
+
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
868
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1408
869
|
});
|
|
1409
|
-
return true;
|
|
1410
870
|
}
|
|
1411
871
|
|
|
1412
|
-
private
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
-
}
|
|
1530
|
-
|
|
1531
|
-
private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
1532
|
-
if (!this.cliScripts?.parseApproval) return null;
|
|
1533
|
-
try {
|
|
1534
|
-
const screenText = this.terminalScreen.getText();
|
|
1535
|
-
const buffer = screenText || this.accumulatedBuffer;
|
|
1536
|
-
return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
|
|
1537
|
-
buffer,
|
|
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;
|
|
872
|
+
private applyParsedSessionMetadata(parsed: any): void {
|
|
873
|
+
const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
|
|
874
|
+
? parsed.providerSessionId.trim()
|
|
875
|
+
: '';
|
|
876
|
+
if (providerSessionId && providerSessionId !== this.providerSessionId) {
|
|
877
|
+
this.providerSessionId = providerSessionId;
|
|
878
|
+
this.updateRuntimeMeta({ providerSessionId });
|
|
1548
879
|
}
|
|
1549
880
|
}
|
|
1550
881
|
|
|
1551
|
-
private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
|
|
1552
|
-
return !!(startupModal || this.activeModal);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
882
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1556
883
|
if (this.parseErrorMessage) return 'error';
|
|
1557
|
-
if (this.
|
|
1558
|
-
if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
|
|
1559
|
-
return this.currentStatus;
|
|
884
|
+
if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
|
|
885
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
|
|
886
|
+
return this.engine.currentStatus;
|
|
1560
887
|
}
|
|
1561
888
|
|
|
1562
889
|
// ─── Public API (CliAdapter) ───────────────────
|
|
@@ -1564,17 +891,73 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1564
891
|
getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
|
|
1565
892
|
const allowParse = options.allowParse !== false;
|
|
1566
893
|
const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
894
|
+
const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
|
|
895
|
+
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
896
|
+
: null;
|
|
1567
897
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1568
|
-
let effectiveModal = startupModal || this.activeModal;
|
|
898
|
+
let effectiveModal = startupModal || this.engine.activeModal;
|
|
899
|
+
// (fix) When we have no captured modal yet, take one more live attempt
|
|
900
|
+
// with the current screen text — the engine's settle pass can miss
|
|
901
|
+
// the modal when it happens to fire exactly between writes, and
|
|
902
|
+
// without a modal here the dashboard could never show the buttons.
|
|
903
|
+
// We deliberately do NOT overwrite an existing engine.activeModal so
|
|
904
|
+
// a stable matched modal wins. This runs even outside the startup
|
|
905
|
+
// gate because Claude's approval frames can appear long after launch.
|
|
906
|
+
if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
|
|
907
|
+
const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
|
|
908
|
+
if (liveDetect === 'waiting_approval') {
|
|
909
|
+
const liveModal = this.runParseApproval(this.terminalScreen.getText())
|
|
910
|
+
|| this.runParseApproval(this.recentOutputBuffer);
|
|
911
|
+
// (fix) Only surface modals that have at least one non-empty
|
|
912
|
+
// button. Half-rendered approval frames produced
|
|
913
|
+
// { message, buttons: [] } briefly — auto-approve would then
|
|
914
|
+
// pick buttonIndex=-1 and resolveModal would write the
|
|
915
|
+
// provider's index-0 key into the prompt. Skip those frames
|
|
916
|
+
// and let a later evaluate find the complete modal.
|
|
917
|
+
const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
|
|
918
|
+
&& liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
|
|
919
|
+
if (liveModal && buttonsOk) {
|
|
920
|
+
effectiveModal = liveModal;
|
|
921
|
+
if (!this.engine.activeModal) this.engine.activeModal = liveModal;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// Only surface waiting_approval when we ALSO have a concrete modal
|
|
926
|
+
// (message + buttons). detectStatus alone can fire while parseApproval
|
|
927
|
+
// is still null — the engine logs "detectStatus=waiting_approval but
|
|
928
|
+
// parseApproval returned null; ignoring". Without this guard getStatus
|
|
929
|
+
// was shipping a bare waiting_approval with activeModal=null and the
|
|
930
|
+
// user perceived the flow as broken.
|
|
931
|
+
if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
|
|
932
|
+
effectiveStatus = 'waiting_approval';
|
|
933
|
+
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
934
|
+
effectiveStatus = 'idle';
|
|
935
|
+
}
|
|
1569
936
|
if (allowParse && !startupModal && !effectiveModal) {
|
|
1570
937
|
const parsed = this.getFreshParsedStatusCache();
|
|
1571
938
|
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
1572
939
|
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
1573
940
|
? parsed.activeModal
|
|
1574
941
|
: null;
|
|
942
|
+
const hasFinalAssistant = (p: any) => {
|
|
943
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
944
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
945
|
+
};
|
|
1575
946
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1576
947
|
effectiveStatus = 'waiting_approval';
|
|
1577
948
|
effectiveModal = parsedModal;
|
|
949
|
+
} else if (
|
|
950
|
+
effectiveStatus === 'idle'
|
|
951
|
+
&& parsed?.status === 'generating'
|
|
952
|
+
&& !hasFinalAssistant(parsed)
|
|
953
|
+
) {
|
|
954
|
+
effectiveStatus = 'generating';
|
|
955
|
+
} else if (
|
|
956
|
+
effectiveStatus === 'generating'
|
|
957
|
+
&& parsed?.status === 'idle'
|
|
958
|
+
&& hasFinalAssistant(parsed)
|
|
959
|
+
) {
|
|
960
|
+
effectiveStatus = 'idle';
|
|
1578
961
|
}
|
|
1579
962
|
}
|
|
1580
963
|
const bufferState = this.getBufferState();
|
|
@@ -1583,8 +966,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1583
966
|
messages: [],
|
|
1584
967
|
workingDir: this.workingDir,
|
|
1585
968
|
activeModal: effectiveModal,
|
|
1586
|
-
|
|
1587
|
-
|
|
969
|
+
approvalEntrySeq: this.engine.approvalEntrySeq,
|
|
970
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
971
|
+
pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
|
|
972
|
+
id: message.id,
|
|
973
|
+
role: message.role,
|
|
974
|
+
content: message.content,
|
|
975
|
+
queuedAt: message.queuedAt,
|
|
976
|
+
source: message.source,
|
|
977
|
+
})),
|
|
978
|
+
errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
|
|
979
|
+
errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
|
|
980
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
981
|
+
lastOutputAt: this.lastOutputAt,
|
|
982
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1588
983
|
...(bufferState ? { bufferState } : {}),
|
|
1589
984
|
};
|
|
1590
985
|
}
|
|
@@ -1600,15 +995,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1600
995
|
const cached = this.parsedStatusCache;
|
|
1601
996
|
const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
|
|
1602
997
|
if (
|
|
1603
|
-
|
|
998
|
+
!this.providerOwnsTranscript()
|
|
999
|
+
&& cached
|
|
1604
1000
|
&& cached.responseBuffer === this.responseBuffer
|
|
1605
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
1001
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1606
1002
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1607
1003
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1608
1004
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1609
1005
|
&& cached.screenText === parseScreenText
|
|
1610
|
-
&& cached.currentStatus === this.currentStatus
|
|
1611
|
-
&& cached.activeModal === this.activeModal
|
|
1006
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
1007
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1612
1008
|
&& cached.cliName === this.cliName
|
|
1613
1009
|
) {
|
|
1614
1010
|
return cached.result;
|
|
@@ -1623,14 +1019,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1623
1019
|
const bufferState = this.getBufferState();
|
|
1624
1020
|
const result = {
|
|
1625
1021
|
id: (parsed as any).id || 'cli_session',
|
|
1626
|
-
status: parsed.status || this.currentStatus,
|
|
1022
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1627
1023
|
title: (parsed as any).title || this.cliName,
|
|
1628
1024
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1629
1025
|
scope: null,
|
|
1630
1026
|
lastOutputAt: this.lastOutputAt,
|
|
1631
1027
|
}),
|
|
1632
1028
|
activeModal,
|
|
1633
|
-
providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
|
|
1029
|
+
providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
|
|
1030
|
+
errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
|
|
1031
|
+
? (parsed as any).errorMessage.trim()
|
|
1032
|
+
: undefined,
|
|
1033
|
+
errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
|
|
1034
|
+
? (parsed as any).errorReason.trim()
|
|
1035
|
+
: undefined,
|
|
1634
1036
|
...(bufferState ? { bufferState } : {}),
|
|
1635
1037
|
...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
|
|
1636
1038
|
? { transcriptAuthority: (parsed as any).transcriptAuthority }
|
|
@@ -1642,13 +1044,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1642
1044
|
|
|
1643
1045
|
this.parsedStatusCache = {
|
|
1644
1046
|
responseBuffer: this.responseBuffer,
|
|
1645
|
-
currentTurnScope: this.currentTurnScope,
|
|
1047
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1646
1048
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1647
1049
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1648
1050
|
accumulatedRawBufferKey,
|
|
1649
1051
|
screenText: parseScreenText,
|
|
1650
|
-
currentStatus: this.currentStatus,
|
|
1651
|
-
activeModal: this.activeModal,
|
|
1052
|
+
currentStatus: this.engine.currentStatus,
|
|
1053
|
+
activeModal: this.engine.activeModal,
|
|
1652
1054
|
cliName: this.cliName,
|
|
1653
1055
|
result,
|
|
1654
1056
|
};
|
|
@@ -1656,22 +1058,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1656
1058
|
}
|
|
1657
1059
|
|
|
1658
1060
|
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
1061
|
const input = buildCliParseInput({
|
|
1664
1062
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1665
1063
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1666
1064
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1667
1065
|
terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
|
|
1066
|
+
workingDir: this.workingDir,
|
|
1067
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1068
|
+
historySessionId: this.providerSessionId || undefined,
|
|
1668
1069
|
baseMessages: [],
|
|
1669
1070
|
partialResponse: this.responseBuffer,
|
|
1670
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1671
|
-
scope: this.currentTurnScope,
|
|
1071
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1072
|
+
scope: this.engine.currentTurnScope,
|
|
1672
1073
|
runtimeSettings: this.runtimeSettings,
|
|
1074
|
+
spawnAt: this.spawnAt,
|
|
1673
1075
|
});
|
|
1674
|
-
return await Promise.resolve(this.
|
|
1076
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1675
1077
|
...input,
|
|
1676
1078
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1677
1079
|
}));
|
|
@@ -1683,7 +1085,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1683
1085
|
|
|
1684
1086
|
/** Whether this adapter has CLI scripts loaded */
|
|
1685
1087
|
hasCliScripts(): boolean {
|
|
1686
|
-
return
|
|
1088
|
+
return this.runner.hasDetectStatus();
|
|
1687
1089
|
}
|
|
1688
1090
|
|
|
1689
1091
|
/**
|
|
@@ -1692,24 +1094,29 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1692
1094
|
*/
|
|
1693
1095
|
async resolveAction(data: any): Promise<void> {
|
|
1694
1096
|
let promptText = '';
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1097
|
+
try {
|
|
1098
|
+
promptText = this.runner.invokeByName('resolveAction', data);
|
|
1099
|
+
} catch {
|
|
1100
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
|
|
1101
|
+
return;
|
|
1701
1102
|
}
|
|
1702
1103
|
if (!promptText) {
|
|
1703
|
-
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not
|
|
1104
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
|
|
1704
1105
|
return;
|
|
1705
1106
|
}
|
|
1706
1107
|
await this.sendMessage(promptText);
|
|
1707
1108
|
}
|
|
1708
1109
|
|
|
1110
|
+
async setInteractivePromptResponse(_response: InteractivePromptResponse): Promise<void> {
|
|
1111
|
+
// Legacy TUI providers do not currently expose an interactive prompt
|
|
1112
|
+
// protocol. Spec-backed claude-cli implements the first real wiring.
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1709
1115
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
1710
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
1711
|
-
if (this.hasActionableApproval()) return false;
|
|
1712
|
-
|
|
1116
|
+
if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
|
|
1117
|
+
if (this.engine.hasActionableApproval()) return false;
|
|
1118
|
+
// If there's already meaningful response content beyond the echoed prompt, not stuck
|
|
1119
|
+
if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
|
|
1713
1120
|
const screenText = this.terminalScreen.getText();
|
|
1714
1121
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
1715
1122
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -1718,20 +1125,41 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1718
1125
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
1719
1126
|
}
|
|
1720
1127
|
|
|
1128
|
+
private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
|
|
1129
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
1130
|
+
if (!raw) return false;
|
|
1131
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1132
|
+
if (!normalizedPrompt) return true;
|
|
1133
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
1134
|
+
if (!normalizedBuffer) return false;
|
|
1135
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1136
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1137
|
+
const remainder = normalizedBuffer
|
|
1138
|
+
.slice(normalizedPrompt.length)
|
|
1139
|
+
.replace(/[─═\-]+/g, '')
|
|
1140
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1141
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1142
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1143
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1144
|
+
.replace(/esctointerrupt/gi, '')
|
|
1145
|
+
.replace(/❯/g, '')
|
|
1146
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1147
|
+
.trim();
|
|
1148
|
+
return remainder.length > 0;
|
|
1149
|
+
}
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1721
1153
|
private async writeToPty(data: string): Promise<void> {
|
|
1722
1154
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1723
1155
|
await this.ptyProcess.write(data);
|
|
1724
1156
|
}
|
|
1725
1157
|
|
|
1726
1158
|
private resetPendingSendState(reason: string): void {
|
|
1727
|
-
this.isWaitingForResponse = false;
|
|
1728
1159
|
this.responseBuffer = '';
|
|
1729
|
-
this.currentTurnScope = null;
|
|
1730
|
-
this.submitPendingUntil = 0;
|
|
1731
|
-
this.clearIdleFinishCandidate(reason);
|
|
1732
1160
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1733
|
-
|
|
1734
|
-
|
|
1161
|
+
this.engine.resetActiveTurnState();
|
|
1162
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
1735
1163
|
}
|
|
1736
1164
|
|
|
1737
1165
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
@@ -1748,42 +1176,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1748
1176
|
}
|
|
1749
1177
|
this.responseTimeout = setTimeout(() => {
|
|
1750
1178
|
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
|
-
});
|
|
1179
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
1768
1180
|
|
|
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
|
|
1181
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
1773
1182
|
// normal settled parser instead and keep the turn open unless the provider
|
|
1774
1183
|
// 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
|
-
});
|
|
1184
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1185
|
+
|
|
1186
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
1787
1187
|
this.armResponseTimeout();
|
|
1788
1188
|
}
|
|
1789
1189
|
}, timeoutMs);
|
|
@@ -1798,34 +1198,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1798
1198
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
1799
1199
|
this.submitRetryTimer = null;
|
|
1800
1200
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1801
|
-
|
|
1802
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1201
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1803
1202
|
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
1203
|
this.writeSubmitKeyForRetry('submit_retry');
|
|
1811
|
-
if (attempt >= 3) { this.submitRetryUsed = true; return; }
|
|
1204
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
1812
1205
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
1813
1206
|
}
|
|
1814
1207
|
|
|
1815
1208
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
1816
1209
|
this.submitRetryTimer = null;
|
|
1817
1210
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1818
|
-
|
|
1819
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1211
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1820
1212
|
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
1213
|
this.writeSubmitKeyForRetry('immediate_retry');
|
|
1828
|
-
this.submitRetryUsed = true;
|
|
1214
|
+
this.engine.submitRetryUsed = true;
|
|
1829
1215
|
}
|
|
1830
1216
|
|
|
1831
1217
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -1833,13 +1219,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1833
1219
|
completion.resolveOnce();
|
|
1834
1220
|
return;
|
|
1835
1221
|
}
|
|
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
|
-
});
|
|
1222
|
+
this.engine.submitPendingUntil = 0;
|
|
1843
1223
|
void this.writeToPty(this.sendKey).then(() => {
|
|
1844
1224
|
this.commitSendUserTurn(state);
|
|
1845
1225
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -1849,13 +1229,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1849
1229
|
}
|
|
1850
1230
|
|
|
1851
1231
|
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
|
-
});
|
|
1232
|
+
this.engine.submitPendingUntil = 0;
|
|
1859
1233
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
1860
1234
|
this.commitSendUserTurn(state);
|
|
1861
1235
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -1899,7 +1273,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1899
1273
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
1900
1274
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
1901
1275
|
};
|
|
1902
|
-
|
|
1276
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1277
|
+
|
|
1903
1278
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
1904
1279
|
// At this point the prompt text write already completed. Rejecting without
|
|
1905
1280
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -1924,12 +1299,145 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1924
1299
|
), 50);
|
|
1925
1300
|
}
|
|
1926
1301
|
|
|
1927
|
-
async sendMessage(text: string): Promise<void> {
|
|
1302
|
+
async sendMessage(text: string, options: { force?: boolean } = {}): Promise<void> {
|
|
1303
|
+
if (options.force === true) {
|
|
1304
|
+
await this.forceSendMessage(text);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
await this.sendMessageNow(text, true);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async forceSendMessage(text: string): Promise<void> {
|
|
1311
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1312
|
+
const content = String(text || '');
|
|
1313
|
+
if (!content.trim()) return;
|
|
1314
|
+
// Modal-park guard (defense-in-depth — the primary guard is at the
|
|
1315
|
+
// cli-provider-instance force-forward chokepoint). A force-write writes raw
|
|
1316
|
+
// keystrokes into the PTY, bypassing the busy send-guard. If the session is
|
|
1317
|
+
// parked on a tool-consent modal, the modal's key handler eats those bytes
|
|
1318
|
+
// and silently resolves an approval the user never made. Hold the write and
|
|
1319
|
+
// let the mesh reconcile loop redeliver once the modal is resolved. We only
|
|
1320
|
+
// hold for an actionable approval modal; plain generating is still force-written
|
|
1321
|
+
// (that is the deadlock the force path exists to break).
|
|
1322
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.hasActionableApproval()) {
|
|
1323
|
+
LOG.info('CLI', `[${this.cliType}] force-send held — session parked on approval modal (status=${this.engine.currentStatus})`);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
|
|
1327
|
+
// Settle, then submit atomically. The previous split-write fix
|
|
1328
|
+
// (write text → settle → write a separate '\r') regressed win32:
|
|
1329
|
+
// ConPTY does not treat a lone CR arriving in its own PTY chunk after a
|
|
1330
|
+
// delay as a submit key, so the Enter was swallowed and the prompt sat
|
|
1331
|
+
// typed-but-unsent. Instead, honor a fixed settle gap so the TUI input
|
|
1332
|
+
// handler is in its input-accepting state, then write `content + sendKey`
|
|
1333
|
+
// as ONE PTY write — the same atomic submit the normal `immediate`
|
|
1334
|
+
// strategy uses. Keeping the Enter in the same write unit as the text is
|
|
1335
|
+
// the invariant that makes win32 ConPTY recognize the submit, and it
|
|
1336
|
+
// also makes the original inject↔submit race impossible (the Enter can
|
|
1337
|
+
// never be separated from the text it submits). The settle still guards
|
|
1338
|
+
// the original concern — that a force-dispatch injected into a freshly
|
|
1339
|
+
// idle session lands before the TUI is ready to accept input.
|
|
1340
|
+
await this.waitForForceSubmitSettle();
|
|
1341
|
+
await this.writeToPty(content + this.sendKey);
|
|
1342
|
+
this.onStatusChange?.();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
private async waitForForceSubmitSettle(): Promise<void> {
|
|
1346
|
+
// A fixed minimum gap so the input handler is ready to accept the paste.
|
|
1347
|
+
// We deliberately do NOT echo-gate here: the submit key is written in the
|
|
1348
|
+
// same atomic PTY write as the text (forceSendMessage), so there is no
|
|
1349
|
+
// separate Enter that could race the echo, and on win32 the echo gate
|
|
1350
|
+
// never reliably matched anyway.
|
|
1351
|
+
await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_SETTLE_MS));
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private enqueuePendingOutboundMessage(text: string, reason: string): void {
|
|
1355
|
+
const content = String(text || '');
|
|
1356
|
+
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
1357
|
+
if (duplicate) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
const queuedAt = Date.now();
|
|
1361
|
+
const message: PendingOutboundMessage = {
|
|
1362
|
+
id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
|
|
1363
|
+
role: 'user',
|
|
1364
|
+
content,
|
|
1365
|
+
queuedAt,
|
|
1366
|
+
source: 'sendMessage',
|
|
1367
|
+
};
|
|
1368
|
+
this.pendingOutboundQueue.push(message);
|
|
1369
|
+
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
1370
|
+
this.onStatusChange?.();
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
1374
|
+
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
1375
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
1376
|
+
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1377
|
+
? String(parsedStatusBeforeSend.status)
|
|
1378
|
+
: '';
|
|
1379
|
+
const hasFinalAssistant = (p: any) => {
|
|
1380
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1381
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1382
|
+
};
|
|
1383
|
+
if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
|
|
1384
|
+
if (this.engine.currentStatus === 'generating') return 'current_status_generating';
|
|
1385
|
+
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating') {
|
|
1386
|
+
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1387
|
+
const parsedHasActionableModal = Boolean(
|
|
1388
|
+
parsedModal
|
|
1389
|
+
&& Array.isArray(parsedModal.buttons)
|
|
1390
|
+
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1391
|
+
);
|
|
1392
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1393
|
+
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1394
|
+
&& !this.engine.isWaitingForResponse
|
|
1395
|
+
&& !this.engine.currentTurnScope
|
|
1396
|
+
&& !this.engine.hasActionableApproval()
|
|
1397
|
+
&& !parsedHasActionableModal;
|
|
1398
|
+
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
1399
|
+
}
|
|
1400
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
private schedulePendingOutboundFlush(delayMs = 0): void {
|
|
1405
|
+
if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
|
|
1406
|
+
this.pendingOutboundFlushTimer = setTimeout(() => {
|
|
1407
|
+
this.pendingOutboundFlushTimer = null;
|
|
1408
|
+
void this.flushPendingOutboundQueue();
|
|
1409
|
+
}, Math.max(0, delayMs));
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
private async flushPendingOutboundQueue(): Promise<void> {
|
|
1413
|
+
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
1414
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
1415
|
+
this.pendingOutboundFlushInFlight = true;
|
|
1416
|
+
try {
|
|
1417
|
+
while (this.pendingOutboundQueue.length > 0) {
|
|
1418
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
1419
|
+
const next = this.pendingOutboundQueue[0];
|
|
1420
|
+
try {
|
|
1421
|
+
await this.sendMessageNow(next.content, false);
|
|
1422
|
+
this.pendingOutboundQueue.shift();
|
|
1423
|
+
this.onStatusChange?.();
|
|
1424
|
+
} catch (error: any) {
|
|
1425
|
+
LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
|
|
1426
|
+
this.schedulePendingOutboundFlush(1000);
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
} finally {
|
|
1431
|
+
this.pendingOutboundFlushInFlight = false;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
|
|
1928
1436
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1929
1437
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
1930
1438
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
1931
|
-
&& this.isWaitingForResponse
|
|
1932
|
-
&& !this.hasActionableApproval();
|
|
1439
|
+
&& this.engine.isWaitingForResponse
|
|
1440
|
+
&& !this.engine.hasActionableApproval();
|
|
1933
1441
|
if (this.startupParseGate) {
|
|
1934
1442
|
const deadline = Date.now() + 10000;
|
|
1935
1443
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -1937,76 +1445,85 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1937
1445
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1938
1446
|
}
|
|
1939
1447
|
}
|
|
1448
|
+
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
1449
|
+
? (() => {
|
|
1450
|
+
try {
|
|
1451
|
+
return this.getScriptParsedStatus?.() || null;
|
|
1452
|
+
} catch {
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
})()
|
|
1456
|
+
: null;
|
|
1457
|
+
const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
|
|
1458
|
+
if (allowQueue && queueReason) {
|
|
1459
|
+
this.enqueuePendingOutboundMessage(text, queueReason);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1940
1462
|
if (!allowInterventionPrompt) {
|
|
1941
1463
|
await this.waitForInteractivePrompt();
|
|
1942
1464
|
}
|
|
1943
1465
|
if (!this.ready) {
|
|
1944
1466
|
this.resolveStartupState('send_precheck');
|
|
1945
|
-
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1467
|
+
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
1946
1468
|
this.ready = true;
|
|
1947
1469
|
this.startupParseGate = false;
|
|
1470
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1948
1471
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
1949
1472
|
}
|
|
1950
1473
|
}
|
|
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;
|
|
1474
|
+
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
1961
1475
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1962
1476
|
? String(parsedStatusBeforeSend.status)
|
|
1963
1477
|
: '';
|
|
1964
|
-
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating')) {
|
|
1478
|
+
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating')) {
|
|
1965
1479
|
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1966
1480
|
const parsedHasActionableModal = Boolean(
|
|
1967
1481
|
parsedModal
|
|
1968
1482
|
&& Array.isArray(parsedModal.buttons)
|
|
1969
1483
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1970
1484
|
);
|
|
1971
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1485
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1972
1486
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1973
|
-
&& !this.isWaitingForResponse
|
|
1974
|
-
&& !this.currentTurnScope
|
|
1975
|
-
&& !this.hasActionableApproval()
|
|
1487
|
+
&& !this.engine.isWaitingForResponse
|
|
1488
|
+
&& !this.engine.currentTurnScope
|
|
1489
|
+
&& !this.engine.hasActionableApproval()
|
|
1976
1490
|
&& !parsedHasActionableModal;
|
|
1977
1491
|
if (!terminalLooksIdle) {
|
|
1492
|
+
if (allowQueue) {
|
|
1493
|
+
this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1978
1496
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1979
1497
|
}
|
|
1980
1498
|
}
|
|
1981
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1982
|
-
|
|
1499
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1500
|
+
const snap = this.getSnapshot();
|
|
1501
|
+
if (
|
|
1502
|
+
!this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
|
|
1503
|
+
&& !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
|
|
1504
|
+
) {
|
|
1505
|
+
if (allowQueue) {
|
|
1506
|
+
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1983
1509
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1984
1510
|
}
|
|
1985
1511
|
}
|
|
1986
|
-
this.isWaitingForResponse = true;
|
|
1987
1512
|
this.responseBuffer = '';
|
|
1988
|
-
|
|
1989
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1990
|
-
this.clearIdleFinishCandidate('send_message');
|
|
1991
|
-
this.currentTurnScope = {
|
|
1513
|
+
const turnScope: TurnParseScope = {
|
|
1992
1514
|
prompt: text,
|
|
1993
1515
|
startedAt: Date.now(),
|
|
1994
1516
|
bufferStart: this.accumulatedBuffer.length,
|
|
1995
1517
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
1996
1518
|
};
|
|
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);
|
|
1519
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2006
1520
|
if (this.submitRetryTimer) {
|
|
2007
1521
|
clearTimeout(this.submitRetryTimer);
|
|
2008
1522
|
this.submitRetryTimer = null;
|
|
2009
1523
|
}
|
|
1524
|
+
this.engine.onTurnStarted(turnScope);
|
|
1525
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1526
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2010
1527
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2011
1528
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2012
1529
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2019,12 +1536,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2019
1536
|
retryDelayMs,
|
|
2020
1537
|
didCommitUserTurn: false,
|
|
2021
1538
|
};
|
|
2022
|
-
|
|
2023
|
-
clearTimeout(this.settleTimer);
|
|
2024
|
-
this.settleTimer = null;
|
|
2025
|
-
}
|
|
2026
|
-
this.responseEpoch += 1;
|
|
2027
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1539
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2028
1540
|
await new Promise<void>((resolve, reject) => {
|
|
2029
1541
|
let resolved = false;
|
|
2030
1542
|
const completion: SendMessageCompletion = {
|
|
@@ -2047,24 +1559,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2047
1559
|
}
|
|
2048
1560
|
|
|
2049
1561
|
if (submitDelayMs > 0) {
|
|
2050
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1562
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2051
1563
|
}
|
|
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
1564
|
const submitStartedAt = Date.now();
|
|
2059
1565
|
void this.writeToPty(text).then(
|
|
2060
1566
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2061
1567
|
completion.rejectOnce,
|
|
2062
1568
|
);
|
|
2063
1569
|
});
|
|
1570
|
+
// Schedule settle after successful send
|
|
1571
|
+
this.engine.scheduleSettle();
|
|
2064
1572
|
}
|
|
2065
1573
|
|
|
2066
1574
|
getPartialResponse(): string {
|
|
2067
|
-
if (!this.isWaitingForResponse) return '';
|
|
1575
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2068
1576
|
return this.responseBuffer;
|
|
2069
1577
|
}
|
|
2070
1578
|
|
|
@@ -2077,10 +1585,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2077
1585
|
cliType: this.cliType,
|
|
2078
1586
|
cliName: this.cliName,
|
|
2079
1587
|
workingDir: this.workingDir,
|
|
2080
|
-
currentStatus: this.currentStatus,
|
|
1588
|
+
currentStatus: this.engine.currentStatus,
|
|
2081
1589
|
ready: this.ready,
|
|
2082
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2083
|
-
activeModal: this.activeModal,
|
|
1590
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1591
|
+
activeModal: this.engine.activeModal,
|
|
2084
1592
|
parseErrorMessage: this.parseErrorMessage,
|
|
2085
1593
|
messageCounts: {
|
|
2086
1594
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2106,10 +1614,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2106
1614
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2107
1615
|
},
|
|
2108
1616
|
parser: {
|
|
2109
|
-
scriptNames:
|
|
2110
|
-
traceSessionId: this.
|
|
2111
|
-
traceSeq: this.
|
|
2112
|
-
currentTurnScope: this.currentTurnScope,
|
|
1617
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1618
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1619
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1620
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2113
1621
|
parsedStatusCache: parsedResult
|
|
2114
1622
|
? {
|
|
2115
1623
|
id: parsedResult.id,
|
|
@@ -2122,26 +1630,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2122
1630
|
activeModal: parsedResult.activeModal,
|
|
2123
1631
|
}
|
|
2124
1632
|
: null,
|
|
2125
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2126
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1633
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1634
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2127
1635
|
},
|
|
2128
1636
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2129
|
-
statusHistory: this.
|
|
2130
|
-
traceEntries: this.
|
|
1637
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1638
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2131
1639
|
timing: {
|
|
2132
1640
|
spawnAt: this.spawnAt,
|
|
2133
1641
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2134
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2135
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2136
|
-
responseEpoch: this.responseEpoch,
|
|
1642
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1643
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1644
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2137
1645
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2138
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1646
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2139
1647
|
},
|
|
2140
1648
|
finish: {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1649
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1650
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1651
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2145
1652
|
},
|
|
2146
1653
|
};
|
|
2147
1654
|
}
|
|
@@ -2151,7 +1658,48 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2151
1658
|
return this.ptyProcess.getMetadata();
|
|
2152
1659
|
}
|
|
2153
1660
|
|
|
1661
|
+
/**
|
|
1662
|
+
* Launch metadata for the dashboard Session info panel. Re-derives the spawn plan
|
|
1663
|
+
* (pure — same inputs the live PTY was spawned with) so the dashboard sees the
|
|
1664
|
+
* resolved binary, full arg vector, and cwd without us having to persist it.
|
|
1665
|
+
* extraEnv values are intentionally dropped (keys only) so secrets passed at
|
|
1666
|
+
* launch time never reach the dashboard.
|
|
1667
|
+
*/
|
|
1668
|
+
getLaunchInfo(): CliLaunchInfo {
|
|
1669
|
+
let command: string | undefined;
|
|
1670
|
+
let args: string[] = [...this.extraArgs];
|
|
1671
|
+
try {
|
|
1672
|
+
const plan = resolveCliSpawnPlan({
|
|
1673
|
+
provider: this.provider,
|
|
1674
|
+
runtimeSettings: this.runtimeSettings,
|
|
1675
|
+
workingDir: this.workingDir,
|
|
1676
|
+
extraArgs: this.extraArgs,
|
|
1677
|
+
extraEnv: this.extraEnv,
|
|
1678
|
+
});
|
|
1679
|
+
command = plan.binaryPath;
|
|
1680
|
+
args = plan.allArgs;
|
|
1681
|
+
} catch {
|
|
1682
|
+
// Spawn plan resolution can throw before the binary is resolvable
|
|
1683
|
+
// (e.g. CLI not installed). Fall back to the raw extra args so the
|
|
1684
|
+
// panel still shows what it can rather than failing the whole call.
|
|
1685
|
+
}
|
|
1686
|
+
return {
|
|
1687
|
+
command,
|
|
1688
|
+
args,
|
|
1689
|
+
extraArgs: [...this.extraArgs],
|
|
1690
|
+
cwd: this.workingDir,
|
|
1691
|
+
extraEnvKeys: Object.keys(this.extraEnv || {}),
|
|
1692
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
2154
1696
|
updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
|
|
1697
|
+
const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
|
|
1698
|
+
? meta.providerSessionId.trim()
|
|
1699
|
+
: '';
|
|
1700
|
+
if (nextProviderSessionId) {
|
|
1701
|
+
this.providerSessionId = nextProviderSessionId;
|
|
1702
|
+
}
|
|
2155
1703
|
if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
|
|
2156
1704
|
this.ptyProcess.updateMeta(meta, replace);
|
|
2157
1705
|
}
|
|
@@ -2172,7 +1720,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2172
1720
|
this.timeouts.shutdownGrace,
|
|
2173
1721
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2174
1722
|
);
|
|
2175
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1723
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2176
1724
|
|
|
2177
1725
|
try {
|
|
2178
1726
|
if (wasProcessing) {
|
|
@@ -2210,7 +1758,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2210
1758
|
return new Promise((resolve) => {
|
|
2211
1759
|
const startedAt = Date.now();
|
|
2212
1760
|
const timer = setInterval(() => {
|
|
2213
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
1761
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2214
1762
|
clearInterval(timer);
|
|
2215
1763
|
resolve(true);
|
|
2216
1764
|
return;
|
|
@@ -2224,18 +1772,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2224
1772
|
}
|
|
2225
1773
|
|
|
2226
1774
|
shutdown(): void {
|
|
2227
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
1775
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2228
1776
|
this.clearAllTimers();
|
|
2229
1777
|
this.pendingOutputParseChunks = [];
|
|
2230
1778
|
this.pendingTerminalQueryTail = '';
|
|
2231
1779
|
this.ptyOutputChunks = [];
|
|
2232
|
-
this.
|
|
1780
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1781
|
+
this.pendingOutboundQueue = [];
|
|
1782
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2233
1783
|
if (this.ptyProcess) {
|
|
2234
1784
|
this.ptyProcess.write('\x03');
|
|
2235
1785
|
setTimeout(() => {
|
|
2236
1786
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2237
1787
|
this.ptyProcess = null;
|
|
2238
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
1788
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2239
1789
|
this.ready = false;
|
|
2240
1790
|
this.startupParseGate = false;
|
|
2241
1791
|
this.spawnAt = 0;
|
|
@@ -2245,12 +1795,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2245
1795
|
}
|
|
2246
1796
|
|
|
2247
1797
|
detach(): void {
|
|
2248
|
-
this.clearIdleFinishCandidate('detach');
|
|
1798
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2249
1799
|
this.clearAllTimers();
|
|
2250
1800
|
this.pendingOutputParseChunks = [];
|
|
2251
1801
|
this.pendingTerminalQueryTail = '';
|
|
2252
1802
|
this.ptyOutputChunks = [];
|
|
2253
|
-
this.
|
|
1803
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1804
|
+
this.pendingOutboundQueue = [];
|
|
1805
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2254
1806
|
if (this.ptyProcess) {
|
|
2255
1807
|
try {
|
|
2256
1808
|
if (typeof this.ptyProcess.detach === 'function') {
|
|
@@ -2268,82 +1820,128 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2268
1820
|
}
|
|
2269
1821
|
|
|
2270
1822
|
clearHistory(): void {
|
|
2271
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
1823
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2272
1824
|
this.accumulatedBuffer = '';
|
|
2273
1825
|
this.accumulatedRawBuffer = '';
|
|
2274
|
-
this.currentTurnScope = null;
|
|
2275
|
-
this.submitRetryUsed = false;
|
|
2276
|
-
this.submitRetryPromptSnippet = '';
|
|
1826
|
+
this.engine.currentTurnScope = null;
|
|
1827
|
+
this.engine.submitRetryUsed = false;
|
|
1828
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2277
1829
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2278
1830
|
this.pendingOutputParseChunks = [];
|
|
2279
1831
|
this.pendingTerminalQueryTail = '';
|
|
2280
1832
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2281
1833
|
this.ptyOutputChunks = [];
|
|
2282
|
-
if (this.
|
|
2283
|
-
this.
|
|
1834
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1835
|
+
this.pendingOutboundQueue = [];
|
|
1836
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2284
1837
|
this.resetTerminalScreen();
|
|
2285
1838
|
this.ptyProcess?.clearBuffer?.();
|
|
2286
1839
|
this.onStatusChange?.();
|
|
2287
1840
|
}
|
|
2288
1841
|
|
|
2289
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
1842
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2290
1843
|
isReady(): boolean { return this.ready; }
|
|
2291
1844
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
1845
|
+
// ─── State machine property accessors (delegate to engine) ──────────────
|
|
1846
|
+
// These expose engine state for external callers (tests, debug tools, etc.)
|
|
1847
|
+
|
|
1848
|
+
get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
|
|
1849
|
+
set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
|
|
1850
|
+
|
|
1851
|
+
get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
|
|
1852
|
+
set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
|
|
1853
|
+
|
|
1854
|
+
get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
|
|
1855
|
+
set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
|
|
1856
|
+
|
|
1857
|
+
get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
|
|
1858
|
+
set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
|
|
1859
|
+
|
|
1860
|
+
get responseEpoch(): number { return this.engine.responseEpoch; }
|
|
1861
|
+
set responseEpoch(v: number) { this.engine.responseEpoch = v; }
|
|
1862
|
+
|
|
1863
|
+
get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
|
|
1864
|
+
set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
|
|
1865
|
+
|
|
1866
|
+
get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
|
|
1867
|
+
set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
|
|
1868
|
+
|
|
1869
|
+
get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
|
|
1870
|
+
set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
|
|
1871
|
+
|
|
1872
|
+
get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
|
|
1873
|
+
set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
|
|
1874
|
+
|
|
1875
|
+
get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
|
|
1876
|
+
set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
|
|
1877
|
+
|
|
1878
|
+
get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
|
|
1879
|
+
get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
|
|
1880
|
+
|
|
1881
|
+
get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
|
|
1882
|
+
get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
|
|
1883
|
+
|
|
1884
|
+
get finishRetryCount(): number { return this.engine.finishRetryCount; }
|
|
1885
|
+
set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
|
|
1886
|
+
|
|
1887
|
+
get traceSessionId(): string { return this.engine.getTraceSessionId(); }
|
|
1888
|
+
get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
|
|
1889
|
+
get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
|
|
1890
|
+
get traceSeq(): number { return this.engine.getTraceEntries().length; }
|
|
1891
|
+
|
|
1892
|
+
/** Expose engine's evaluateSettled for test access */
|
|
1893
|
+
evaluateSettled(): void {
|
|
1894
|
+
LOG.debug(
|
|
1895
|
+
'CLI',
|
|
1896
|
+
`[${this.cliType}] settled diagnostics delegated to state engine`);
|
|
1897
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1898
|
+
}
|
|
1899
|
+
/** Expose engine's scheduleSettle for test access */
|
|
1900
|
+
scheduleSettle(): void { this.engine.scheduleSettle(); }
|
|
1901
|
+
/** Expose engine's clearIdleFinishCandidate for test access */
|
|
1902
|
+
clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
|
|
1903
|
+
/** Expose engine's finishResponse for test access */
|
|
1904
|
+
finishResponse(): void { this.engine.finishResponse(); }
|
|
1905
|
+
/** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
|
|
1906
|
+
getSnapshot(): CliBufferSnapshot {
|
|
1907
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
1908
|
+
return {
|
|
1909
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
1910
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1911
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
1912
|
+
responseBuffer: this.responseBuffer,
|
|
1913
|
+
screenText,
|
|
1914
|
+
parseScreenText: this.getParseScreenText(screenText),
|
|
1915
|
+
workingDir: this.workingDir,
|
|
1916
|
+
providerSessionId: this.providerSessionId,
|
|
1917
|
+
runtimeSettings: this.runtimeSettings,
|
|
1918
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1919
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1920
|
+
lastOutputAt: this.lastOutputAt,
|
|
1921
|
+
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
1922
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1923
|
+
lastScreenSnapshot: this.lastScreenSnapshot,
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
isAlive(): boolean { return this.ptyProcess !== null; }
|
|
1927
|
+
flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
|
|
1928
|
+
|
|
1929
|
+
async writeRaw(data: string | Buffer): Promise<void> {
|
|
1930
|
+
const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
|
1931
|
+
await this.writeToPty(str);
|
|
2298
1932
|
}
|
|
2299
1933
|
|
|
2300
1934
|
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
|
-
}
|
|
1935
|
+
this.engine.resolveModal(buttonIndex);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
1939
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
1943
|
+
isApprovalRecentlyResolved(): boolean {
|
|
1944
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2347
1945
|
}
|
|
2348
1946
|
|
|
2349
1947
|
resize(cols: number, rows: number): void {
|
|
@@ -2357,7 +1955,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2357
1955
|
}
|
|
2358
1956
|
|
|
2359
1957
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2360
|
-
if (this.startupParseGate ||
|
|
1958
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2361
1959
|
try {
|
|
2362
1960
|
const parsed = this.getScriptParsedStatus();
|
|
2363
1961
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2369,18 +1967,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2369
1967
|
getDebugState(): Record<string, any> {
|
|
2370
1968
|
const screenText = sanitizeTerminalText(this.terminalScreen.getText());
|
|
2371
1969
|
const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
2372
|
-
const
|
|
2373
|
-
|
|
1970
|
+
const startupDetectedStatus = this.startupParseGate && !startupModal
|
|
1971
|
+
? this.runDetectStatus(this.recentOutputBuffer || screenText)
|
|
1972
|
+
: null;
|
|
1973
|
+
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2374
1974
|
const parsedDebugState = this.getParsedDebugState();
|
|
2375
1975
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
1976
|
+
const hasFinalAssistant = (p: any) => {
|
|
1977
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1978
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1979
|
+
};
|
|
1980
|
+
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1981
|
+
if (parsedDebugState?.status === 'error') {
|
|
1982
|
+
effectiveStatus = 'error';
|
|
1983
|
+
}
|
|
1984
|
+
// (fix) Mirror the getStatus contract: never surface waiting_approval
|
|
1985
|
+
// without a concrete modal. detectStatus alone can fire while
|
|
1986
|
+
// parseApproval is still null, and a bare debugState waiting_approval
|
|
1987
|
+
// confused dashboards reading the debug payload.
|
|
1988
|
+
const debugEffectiveModal = startupModal || this.engine.activeModal;
|
|
1989
|
+
if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
|
|
1990
|
+
effectiveStatus = 'waiting_approval';
|
|
1991
|
+
}
|
|
1992
|
+
if (
|
|
1993
|
+
effectiveStatus === 'idle'
|
|
1994
|
+
&& parsedDebugState?.status === 'generating'
|
|
1995
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
1996
|
+
) {
|
|
1997
|
+
effectiveStatus = 'generating';
|
|
1998
|
+
}
|
|
2376
1999
|
return {
|
|
2377
2000
|
type: this.cliType,
|
|
2378
2001
|
name: this.cliName,
|
|
2379
2002
|
providerResolution: this.providerResolutionMeta,
|
|
2380
2003
|
status: effectiveStatus,
|
|
2381
2004
|
projectedStatus: effectiveStatus,
|
|
2382
|
-
rawStatus: this.currentStatus,
|
|
2383
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2005
|
+
rawStatus: this.engine.currentStatus,
|
|
2006
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2384
2007
|
ready: effectiveReady,
|
|
2385
2008
|
startupParseGate: this.startupParseGate,
|
|
2386
2009
|
spawnAt: this.spawnAt,
|
|
@@ -2394,38 +2017,47 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2394
2017
|
providerSessionId: parsedDebugState.providerSessionId,
|
|
2395
2018
|
transcriptAuthority: parsedDebugState.transcriptAuthority,
|
|
2396
2019
|
coverage: parsedDebugState.coverage,
|
|
2020
|
+
errorMessage: parsedDebugState.errorMessage,
|
|
2021
|
+
errorReason: parsedDebugState.errorReason,
|
|
2397
2022
|
activeModal: parsedDebugState.activeModal,
|
|
2398
2023
|
messageCount: parsedMessages.length,
|
|
2399
2024
|
} : null,
|
|
2400
2025
|
screenText: screenText.slice(-4000),
|
|
2401
|
-
currentTurnScope: this.currentTurnScope,
|
|
2026
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2402
2027
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2403
2028
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2404
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2405
2029
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2406
2030
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2407
2031
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
2408
2032
|
sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
|
|
2409
2033
|
responseBuffer: this.responseBuffer.slice(-1000),
|
|
2034
|
+
pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
|
|
2035
|
+
id: message.id,
|
|
2036
|
+
role: message.role,
|
|
2037
|
+
content: message.content,
|
|
2038
|
+
queuedAt: message.queuedAt,
|
|
2039
|
+
source: message.source,
|
|
2040
|
+
})),
|
|
2041
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
2410
2042
|
lastOutputAt: this.lastOutputAt,
|
|
2411
2043
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2412
2044
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2413
2045
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2414
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2415
|
-
activeModal: startupModal || this.activeModal,
|
|
2416
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
2046
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
2047
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
2048
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2417
2049
|
sendDelayMs: this.sendDelayMs,
|
|
2418
2050
|
sendKey: this.sendKey,
|
|
2419
2051
|
submitStrategy: this.submitStrategy,
|
|
2420
2052
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2421
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2422
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2053
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
2054
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2423
2055
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2424
2056
|
hasCliScripts: this.hasCliScripts(),
|
|
2425
|
-
scriptNames:
|
|
2426
|
-
traceSessionId: this.
|
|
2427
|
-
traceEntryCount: this.
|
|
2428
|
-
statusHistory: this.
|
|
2057
|
+
scriptNames: this.runner.getScriptNames(),
|
|
2058
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
2059
|
+
traceEntryCount: this.engine.getTraceEntries().length,
|
|
2060
|
+
statusHistory: this.engine.getStatusHistory().slice(-30),
|
|
2429
2061
|
timeouts: this.timeouts,
|
|
2430
2062
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2431
2063
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2435,20 +2067,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2435
2067
|
|
|
2436
2068
|
getTraceState(limit = 120): Record<string, any> {
|
|
2437
2069
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
2070
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2438
2071
|
return {
|
|
2439
|
-
sessionId: this.
|
|
2072
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2440
2073
|
providerResolution: this.providerResolutionMeta,
|
|
2441
|
-
entryCount:
|
|
2442
|
-
entries:
|
|
2074
|
+
entryCount: traceEntries.length,
|
|
2075
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2443
2076
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2444
2077
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2445
2078
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2446
2079
|
status: this.projectEffectiveStatus(),
|
|
2447
2080
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2448
|
-
rawStatus: this.currentStatus,
|
|
2449
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2450
|
-
activeModal: this.activeModal,
|
|
2451
|
-
currentTurnScope: this.currentTurnScope,
|
|
2081
|
+
rawStatus: this.engine.currentStatus,
|
|
2082
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2083
|
+
activeModal: this.engine.activeModal,
|
|
2084
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2452
2085
|
messages: [],
|
|
2453
2086
|
};
|
|
2454
2087
|
}
|