@adhdev/daemon-core 0.9.82-rc.40 → 0.9.82-rc.400
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 +9 -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 +67 -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 +123 -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 +74 -0
- package/dist/cli-adapters/pty-write-chunking.d.ts +34 -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/chat-commands-debug-bundle.d.ts +14 -0
- package/dist/commands/chat-commands-read.d.ts +7 -0
- package/dist/commands/chat-commands-scope.d.ts +39 -0
- package/dist/commands/chat-commands-shared.d.ts +33 -0
- package/dist/commands/chat-commands-write.d.ts +14 -0
- package/dist/commands/chat-commands.d.ts +10 -23
- package/dist/commands/cli-manager.d.ts +15 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +60 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -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/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +2 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +116 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +256 -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/config/mesh-json-config.d.ts +299 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- 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/git-worktree.d.ts +22 -0
- package/dist/git/index.d.ts +2 -0
- package/dist/index.d.ts +57 -13
- package/dist/index.js +45829 -17086
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45623 -17021
- 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 +81 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +89 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +174 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-event-classify.d.ts +5 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +19 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
- package/dist/mesh/mesh-events-pending.d.ts +55 -0
- package/dist/mesh/mesh-events-stale.d.ts +61 -0
- package/dist/mesh/mesh-events-utils.d.ts +75 -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 -4
- package/dist/mesh/mesh-ledger.d.ts +77 -1
- package/dist/mesh/mesh-missions.d.ts +164 -0
- package/dist/mesh/mesh-node-identity.d.ts +289 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +95 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +65 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +428 -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 +447 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -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-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +247 -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 +145 -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 +13 -1
- package/dist/providers/cli-provider-instance.d.ts +157 -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/constants.d.ts +12 -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 +407 -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 +420 -13
- package/dist/runtime-defaults.d.ts +2 -0
- 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/hash.d.ts +8 -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 +63 -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 +68 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1221 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +888 -1144
- 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 +121 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/pty-write-chunking.ts +106 -0
- 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-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2327 -0
- package/src/commands/chat-commands-scope.ts +54 -0
- package/src/commands/chat-commands-shared.ts +114 -0
- package/src/commands/chat-commands-write.ts +880 -0
- package/src/commands/chat-commands.ts +19 -1841
- package/src/commands/cli-manager.ts +513 -26
- package/src/commands/handler.ts +841 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +673 -0
- package/src/commands/high-family/mesh-events.ts +80 -0
- package/src/commands/high-family/mesh-status.ts +678 -0
- package/src/commands/high-family/types.ts +76 -0
- package/src/commands/low-family/coordinator-prompt.ts +72 -0
- package/src/commands/low-family/daemon-lifecycle.ts +107 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +87 -0
- package/src/commands/low-family/notification.ts +116 -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/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/med-family/cli-agent.ts +238 -0
- package/src/commands/med-family/fast-forward.ts +230 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +37 -0
- package/src/commands/med-family/mesh-crud.ts +865 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +131 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +120 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router.ts +2516 -3221
- 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 +353 -22
- package/src/config/mesh-json-config.ts +617 -0
- 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/daemon/dev-server.ts +0 -541
- 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 +76 -1
- package/src/git/index.ts +16 -0
- package/src/index.ts +171 -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 +341 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +338 -0
- package/src/mesh/coordinator-prompt.ts +381 -33
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +645 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-event-classify.ts +51 -0
- package/src/mesh/mesh-event-forwarding.ts +1762 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +31 -0
- package/src/mesh/mesh-events-pending.ts +590 -0
- package/src/mesh/mesh-events-stale.ts +389 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +32 -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 -7
- package/src/mesh/mesh-ledger.ts +596 -102
- package/src/mesh/mesh-missions.ts +322 -0
- package/src/mesh/mesh-node-identity.ts +1887 -0
- package/src/mesh/mesh-queue-assignment.ts +1726 -0
- package/src/mesh/mesh-reconcile-loop.ts +1550 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1660 -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 +1817 -0
- package/src/mesh/mesh-scheduling-runtime.ts +201 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1141 -141
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +423 -0
- package/src/mesh/worktree-bootstrap-config.ts +369 -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 +46 -6
- package/src/providers/cli-provider-instance.ts +1580 -103
- 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/constants.ts +19 -0
- package/src/providers/native-history/dispatcher.ts +339 -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 +668 -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 +1056 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1304 -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 +1128 -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 +629 -14
- package/src/runtime-defaults.ts +39 -0
- 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/hash.ts +23 -0
- 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,16 +15,24 @@
|
|
|
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,
|
|
25
27
|
type PtyRuntimeTransport,
|
|
26
28
|
type PtyTransportFactory,
|
|
27
29
|
} from './pty-transport.js';
|
|
30
|
+
import {
|
|
31
|
+
WIN32_PTY_WRITE_CHUNK_CHARS,
|
|
32
|
+
WIN32_PTY_WRITE_CHUNK_GAP_MS,
|
|
33
|
+
chunkPreservingSurrogates,
|
|
34
|
+
shouldChunkWin32Write,
|
|
35
|
+
} from './pty-write-chunking.js';
|
|
28
36
|
import {
|
|
29
37
|
buildCliScreenSnapshot,
|
|
30
38
|
compactPromptText,
|
|
@@ -44,6 +52,13 @@ import {
|
|
|
44
52
|
type CliTraceEntry,
|
|
45
53
|
type ParsedSession,
|
|
46
54
|
} from './provider-cli-shared.js';
|
|
55
|
+
import { CliScriptRunner } from './cli-script-runner.js';
|
|
56
|
+
import {
|
|
57
|
+
CliStateEngine,
|
|
58
|
+
type CliBufferSnapshot,
|
|
59
|
+
type CliTransportAccess,
|
|
60
|
+
type CliStateEngineCallbacks,
|
|
61
|
+
} from './cli-state-engine.js';
|
|
47
62
|
import {
|
|
48
63
|
buildCliParseInput,
|
|
49
64
|
buildCliTraceParseSnapshot,
|
|
@@ -78,24 +93,6 @@ export {
|
|
|
78
93
|
} from './provider-cli-shared.js';
|
|
79
94
|
|
|
80
95
|
|
|
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
96
|
interface SendMessageState {
|
|
100
97
|
text: string;
|
|
101
98
|
normalizedPromptSnippet: string;
|
|
@@ -103,6 +100,10 @@ interface SendMessageState {
|
|
|
103
100
|
maxEchoWaitMs: number;
|
|
104
101
|
retryDelayMs: number;
|
|
105
102
|
didCommitUserTurn: boolean;
|
|
103
|
+
// Whether this was the session's first turn at the moment of dispatch — captured
|
|
104
|
+
// before commitSendUserTurn flips this.firstTurnSent, so a later stuck-retry still
|
|
105
|
+
// knows it is recovering the win32 premature-ready first-turn swallow.
|
|
106
|
+
isFirstTurn: boolean;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
interface SendMessageCompletion {
|
|
@@ -110,6 +111,14 @@ interface SendMessageCompletion {
|
|
|
110
111
|
rejectOnce: (error: unknown) => void;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
interface PendingOutboundMessage {
|
|
115
|
+
id: string;
|
|
116
|
+
role: 'user';
|
|
117
|
+
content: string;
|
|
118
|
+
queuedAt: number;
|
|
119
|
+
source: 'sendMessage';
|
|
120
|
+
}
|
|
121
|
+
|
|
113
122
|
export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
|
|
114
123
|
if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
|
|
115
124
|
if (maxChars <= 0) return '';
|
|
@@ -119,27 +128,63 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
|
|
|
119
128
|
return current.slice(-keepFromCurrent) + chunk;
|
|
120
129
|
}
|
|
121
130
|
|
|
131
|
+
// Force-send (mesh coordinator dispatch / reconcile redelivery) writes raw
|
|
132
|
+
// keystrokes straight into the PTY, bypassing the normal echo-wait submit
|
|
133
|
+
// pipeline.
|
|
134
|
+
//
|
|
135
|
+
// History of this path's failure modes:
|
|
136
|
+
// 1. Originally it wrote `text + sendKey` in a single PTY write with zero
|
|
137
|
+
// settle time. Injected into an idle TUI input box that was still entering
|
|
138
|
+
// its input-accepting state, the trailing submit key could be swallowed —
|
|
139
|
+
// prompt typed but never submitted ("text injected, Enter not pressed").
|
|
140
|
+
// 2. The first fix split inject from submit: write(text) → echo-gated settle →
|
|
141
|
+
// write(sendKey) as a *separate* PTY write. That broke win32: ConPTY/TUI
|
|
142
|
+
// does not recognize a lone '\r' that arrives in its own PTY chunk
|
|
143
|
+
// 150ms+ after the text as a submit key, and the win32 echo gate never
|
|
144
|
+
// matched (ConPTY echo/parsing differences), so the cap was burned and the
|
|
145
|
+
// same detached lone CR was emitted — adding latency while the Enter still
|
|
146
|
+
// got swallowed.
|
|
147
|
+
//
|
|
148
|
+
// Current behavior: keep a fixed settle gap so the input handler is in its
|
|
149
|
+
// input-accepting state, then write `content + sendKey` as a SINGLE atomic PTY
|
|
150
|
+
// write — the same verified mechanism the normal `immediate` submit strategy
|
|
151
|
+
// uses (submitImmediatePrompt). With the Enter in the same write unit as the
|
|
152
|
+
// text, win32 ConPTY always sees it as a submit and the race that the split was
|
|
153
|
+
// trying to solve cannot happen (the Enter can never be separated from the
|
|
154
|
+
// text). FORCE_SUBMIT_SETTLE_MS is that minimum pre-submit gap.
|
|
155
|
+
const FORCE_SUBMIT_SETTLE_MS = 150;
|
|
156
|
+
|
|
122
157
|
// ─── Adapter ────────────────────────────────────────
|
|
123
158
|
|
|
124
159
|
export class ProviderCliAdapter implements CliAdapter {
|
|
125
|
-
|
|
160
|
+
cliType: string;
|
|
126
161
|
readonly cliName: string;
|
|
127
162
|
public workingDir: string;
|
|
128
163
|
|
|
129
164
|
private provider: CliProviderModule;
|
|
130
165
|
private ptyProcess: PtyRuntimeTransport | null = null;
|
|
131
166
|
private transportFactory: PtyTransportFactory;
|
|
132
|
-
private currentStatus: CliSessionStatus['status'] = 'starting';
|
|
133
167
|
private onStatusChange: (() => void) | null = null;
|
|
134
168
|
|
|
169
|
+
// ─── State machine engine ─────────────────────────
|
|
170
|
+
readonly engine: CliStateEngine;
|
|
171
|
+
|
|
135
172
|
private responseBuffer = '';
|
|
136
173
|
private recentOutputBuffer = '';
|
|
137
|
-
private
|
|
138
|
-
private
|
|
139
|
-
private parseErrorMessage: string | null = null;
|
|
174
|
+
private get parseErrorMessage(): string | null { return this.runner.parseErrorMessage; }
|
|
175
|
+
private providerSessionId: string | null = null;
|
|
140
176
|
private responseTimeout: NodeJS.Timeout | null = null;
|
|
141
|
-
private idleTimeout: NodeJS.Timeout | null = null;
|
|
142
177
|
private ready = false;
|
|
178
|
+
// WIN32-READY-HOLD: the ready barrier can release on screen/spec-FSM grace
|
|
179
|
+
// before win32 ConPTY's input layer is live. The first split write (text, then a
|
|
180
|
+
// separate trailing CR via waitForEchoAndSubmit) then has its submit CR swallowed,
|
|
181
|
+
// and every CR-only retry re-sends a bare CR the input layer keeps dropping — the
|
|
182
|
+
// first message is typed-but-never-submitted and lost. Routing only the FIRST turn
|
|
183
|
+
// through the atomic content+sendKey single write (submitImmediatePrompt) keeps the
|
|
184
|
+
// Enter in the same PTY write unit as the text, the invariant win32 ConPTY needs to
|
|
185
|
+
// recognize a submit, so the swallow is bypassed. Subsequent turns (input layer now
|
|
186
|
+
// proven live) keep the normal echo-gated path. Flips true on first committed turn.
|
|
187
|
+
private firstTurnSent = false;
|
|
143
188
|
private startupBuffer = '';
|
|
144
189
|
private startupParseGate = false;
|
|
145
190
|
private startupSettleTimer: NodeJS.Timeout | null = null;
|
|
@@ -164,39 +209,48 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
164
209
|
private serverConn: any = null;
|
|
165
210
|
private logBuffer: { message: string; level: string }[] = [];
|
|
166
211
|
|
|
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;
|
|
212
|
+
private pendingOutboundQueue: PendingOutboundMessage[] = [];
|
|
213
|
+
private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
|
|
214
|
+
private pendingOutboundFlushInFlight = false;
|
|
215
|
+
// Submit retry timer — PTY-level, not state machine
|
|
183
216
|
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
217
|
|
|
190
218
|
// Resize redraw suppression
|
|
191
219
|
private resizeSuppressUntil: number = 0;
|
|
192
220
|
|
|
193
|
-
|
|
194
|
-
|
|
221
|
+
// (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
|
|
222
|
+
// ChatSourceMachine holds the lock by state, not by a mutable field on
|
|
223
|
+
// the adapter. Removed entirely; no callers remain after the readChat
|
|
224
|
+
// ladder was replaced.
|
|
225
|
+
|
|
226
|
+
// ─── Script runner (parsing isolated here, adapter stays as transport) ───
|
|
227
|
+
private readonly runner: CliScriptRunner;
|
|
228
|
+
/** @deprecated use runner.cliScripts for direct script access */
|
|
229
|
+
get cliScripts(): CliScripts { return this.runner.cliScripts; }
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Recent script invocations (oldest → newest). Exposed via the
|
|
233
|
+
* `get_chat_debug_bundle` daemon command so anyone debugging a
|
|
234
|
+
* stuck-status regression can read what each detectStatus /
|
|
235
|
+
* parseSession call actually saw and returned, instead of having
|
|
236
|
+
* to instrument the daemon.
|
|
237
|
+
*/
|
|
238
|
+
getScriptInvocationTrace() {
|
|
239
|
+
return this.runner.getInvocationTrace();
|
|
240
|
+
}
|
|
195
241
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Returns the full raw PTY byte stream captured since adapter start.
|
|
244
|
+
* Used by the `record_provider_pty` IPC command to produce fixtures.
|
|
245
|
+
* Bounded by MAX_ACCUMULATED_BUFFER; older bytes may have been dropped.
|
|
246
|
+
*/
|
|
247
|
+
getAccumulatedRawBuffer(): { text: string; droppedChars: number } {
|
|
248
|
+
return {
|
|
249
|
+
text: this.accumulatedRawBuffer,
|
|
250
|
+
droppedChars: this.accumulatedRawBufferDroppedChars,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
|
|
200
254
|
private runtimeSettings: Record<string, any> = {};
|
|
201
255
|
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
202
256
|
private accumulatedBuffer: string = '';
|
|
@@ -205,7 +259,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
205
259
|
/** Full accumulated raw PTY output (with ANSI) */
|
|
206
260
|
private accumulatedRawBuffer: string = '';
|
|
207
261
|
/** Current visible terminal screen snapshot */
|
|
208
|
-
private terminalScreen = new TerminalScreen(
|
|
262
|
+
private terminalScreen = new TerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
|
|
209
263
|
private static readonly MAX_RESPONSE_BUFFER = 8000;
|
|
210
264
|
private static readonly MAX_RECENT_OUTPUT_BUFFER = 1000;
|
|
211
265
|
private responseBufferDroppedChars = 0;
|
|
@@ -216,10 +270,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
216
270
|
* Hermes turn (tool calls + reasoning + final bubble) without the
|
|
217
271
|
* rolling window pushing the turn's ╭─ opening line out of view. */
|
|
218
272
|
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
273
|
private parsedStatusCache: {
|
|
224
274
|
responseBuffer: string;
|
|
225
275
|
currentTurnScope: TurnParseScope | null;
|
|
@@ -233,11 +283,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
233
283
|
result: any;
|
|
234
284
|
} | null = null;
|
|
235
285
|
private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
236
|
-
private static readonly MAX_TRACE_ENTRIES = 250;
|
|
237
286
|
|
|
238
287
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
239
|
-
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
240
|
-
private static readonly MAX_FINISH_RETRIES = 2;
|
|
241
288
|
|
|
242
289
|
private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
|
|
243
290
|
const build = (droppedChars: number, maxChars: number) => droppedChars > 0
|
|
@@ -313,13 +360,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
313
360
|
if (
|
|
314
361
|
cached
|
|
315
362
|
&& cached.responseBuffer === this.responseBuffer
|
|
316
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
363
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
317
364
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
318
365
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
319
366
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
320
367
|
&& cached.screenText === this.lastScreenText
|
|
321
|
-
&& cached.currentStatus === this.currentStatus
|
|
322
|
-
&& cached.activeModal === this.activeModal
|
|
368
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
369
|
+
&& cached.activeModal === this.engine.activeModal
|
|
323
370
|
&& cached.cliName === this.cliName
|
|
324
371
|
) {
|
|
325
372
|
return cached.result;
|
|
@@ -343,86 +390,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
343
390
|
return this.timeouts.statusActivityHold;
|
|
344
391
|
}
|
|
345
392
|
|
|
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
393
|
// Resolved timeouts
|
|
427
394
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
428
395
|
|
|
@@ -432,7 +399,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
432
399
|
private readonly sendKey: string;
|
|
433
400
|
private readonly submitStrategy: 'wait_for_echo' | 'immediate';
|
|
434
401
|
private readonly requirePromptEchoBeforeSubmit: boolean;
|
|
435
|
-
private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
|
|
436
402
|
|
|
437
403
|
constructor(
|
|
438
404
|
provider: CliProviderModule,
|
|
@@ -441,6 +407,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
441
407
|
private extraEnv: Record<string, string> = {},
|
|
442
408
|
transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
|
|
443
409
|
) {
|
|
410
|
+
this.runner = new CliScriptRunner(provider.type);
|
|
444
411
|
this.provider = provider;
|
|
445
412
|
this.transportFactory = transportFactory;
|
|
446
413
|
this.cliType = provider.type;
|
|
@@ -458,10 +425,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
458
425
|
this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
|
|
459
426
|
this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
|
|
460
427
|
|
|
461
|
-
//
|
|
462
|
-
this.
|
|
463
|
-
|
|
464
|
-
|
|
428
|
+
// State machine engine — owns all status transitions
|
|
429
|
+
this.engine = new CliStateEngine(
|
|
430
|
+
provider,
|
|
431
|
+
this.runner,
|
|
432
|
+
this as unknown as CliTransportAccess,
|
|
433
|
+
{
|
|
434
|
+
onStatusChange: () => { this.onStatusChange?.(); },
|
|
435
|
+
onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
|
|
436
|
+
onTurnCompleted: () => { this.responseBuffer = ''; },
|
|
437
|
+
} satisfies CliStateEngineCallbacks,
|
|
438
|
+
resolvedConfig.timeouts,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// Scripts delegated to CliScriptRunner — adapter stays as transport.
|
|
442
|
+
// Pass the manifest tui block so the runner can build the
|
|
443
|
+
// declarativeDetectStatus / declarativeParseApproval SDK functions
|
|
444
|
+
// that v1 extended-tier overrides depend on (without these, scripts
|
|
445
|
+
// like codex-cli's detect_status v1 fail-closed with return 'idle'
|
|
446
|
+
// and the session sticks in `generating` forever).
|
|
447
|
+
this.runner.setScripts(provider.scripts || {}, provider.tui);
|
|
448
|
+
// Per-invocation wall-clock budget for provider scripts. Manifests may
|
|
449
|
+
// raise this for genuinely slow parsers, but the default (50ms) is the
|
|
450
|
+
// settle-loop safety net — any script that exceeds it gets flagged in
|
|
451
|
+
// the invocation trace and surfaced via the debug bundle so we can
|
|
452
|
+
// identify the offender instead of guessing why the settle loop hangs.
|
|
453
|
+
this.runner.setScriptCallBudget(provider.scriptCallBudgetMs ?? 50);
|
|
454
|
+
const scriptNames = this.runner.getScriptNames();
|
|
465
455
|
if (scriptNames.length > 0) {
|
|
466
456
|
LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
|
|
467
457
|
LOG.info(
|
|
@@ -487,14 +477,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
487
477
|
|
|
488
478
|
/** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
|
|
489
479
|
setCliScripts(scripts: CliScripts): void {
|
|
490
|
-
this.
|
|
480
|
+
this.runner.setScripts(scripts);
|
|
491
481
|
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(', ')}]`);
|
|
482
|
+
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
|
|
498
483
|
}
|
|
499
484
|
|
|
500
485
|
/** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
|
|
@@ -548,15 +533,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
548
533
|
});
|
|
549
534
|
|
|
550
535
|
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
536
|
|
|
561
537
|
try {
|
|
562
538
|
this.ptyProcess = this.transportFactory.spawn(
|
|
@@ -620,13 +596,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
620
596
|
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
621
597
|
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
|
|
622
598
|
this.flushPendingOutputParse();
|
|
623
|
-
this.recordTrace('exit', { exitCode });
|
|
624
599
|
this.ptyProcess = null;
|
|
625
|
-
this.
|
|
600
|
+
this.engine.onPtyExit();
|
|
626
601
|
this.ready = false;
|
|
627
602
|
this.startupParseGate = false;
|
|
628
603
|
this.spawnAt = 0;
|
|
629
|
-
this.
|
|
604
|
+
this.runner.resetSessionState();
|
|
630
605
|
this.onStatusChange?.();
|
|
631
606
|
});
|
|
632
607
|
|
|
@@ -635,17 +610,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
635
610
|
this.startupBuffer = '';
|
|
636
611
|
this.startupFirstOutputAt = 0;
|
|
637
612
|
if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
|
|
638
|
-
this.resetTerminalScreen(
|
|
613
|
+
this.resetTerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
|
|
639
614
|
this.pendingTerminalQueryTail = '';
|
|
640
|
-
this.currentTurnScope = null;
|
|
641
|
-
this.finishRetryCount = 0;
|
|
642
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
643
615
|
this.ready = false;
|
|
616
|
+
// Each fresh spawn re-enters the premature-ready swallow window — the next
|
|
617
|
+
// turn is again a "first turn" and must use the win32-safe atomic send path.
|
|
618
|
+
this.firstTurnSent = false;
|
|
644
619
|
await this.ptyProcess.ready;
|
|
645
|
-
this.
|
|
646
|
-
runtimeMeta: this.getRuntimeMetadata(),
|
|
647
|
-
});
|
|
648
|
-
this.setStatus('starting', 'pty_ready');
|
|
620
|
+
this.engine.onSpawnReady();
|
|
649
621
|
this.scheduleStartupSettleCheck();
|
|
650
622
|
this.onStatusChange?.();
|
|
651
623
|
}
|
|
@@ -671,11 +643,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
671
643
|
if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
|
|
672
644
|
this.startupFirstOutputAt = now;
|
|
673
645
|
}
|
|
674
|
-
if (
|
|
675
|
-
this.clearIdleFinishCandidate('new_output');
|
|
646
|
+
if (rawData.length > 0 || cleanData.length > 0) {
|
|
647
|
+
this.engine.clearIdleFinishCandidate('new_output');
|
|
676
648
|
}
|
|
677
649
|
if (getDebugRuntimeConfig().collectDebugTrace) {
|
|
678
|
-
this.
|
|
650
|
+
this.engine.recordExternalTrace('output', {
|
|
679
651
|
rawLength: rawData.length,
|
|
680
652
|
cleanLength: cleanData.length,
|
|
681
653
|
rawPreview: summarizeCliTraceText(rawData, 300),
|
|
@@ -687,7 +659,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
687
659
|
this.scheduleStartupSettleCheck();
|
|
688
660
|
}
|
|
689
661
|
|
|
690
|
-
if (this.isWaitingForResponse && cleanData) {
|
|
662
|
+
if (this.engine.isWaitingForResponse && cleanData) {
|
|
691
663
|
const previousResponseLen = this.responseBuffer.length;
|
|
692
664
|
this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
|
|
693
665
|
this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
|
|
@@ -726,19 +698,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
726
698
|
// Keep turn-scope offsets aligned with the truncated buffer so scoped
|
|
727
699
|
// parses don't lose the beginning of a long turn (e.g. the Hermes
|
|
728
700
|
// ╭─ opening line) when the rolling window sheds bytes.
|
|
729
|
-
if (this.currentTurnScope) {
|
|
701
|
+
if (this.engine.currentTurnScope) {
|
|
730
702
|
if (droppedClean > 0) {
|
|
731
|
-
this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
|
|
703
|
+
this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
|
|
732
704
|
}
|
|
733
705
|
if (droppedRaw > 0) {
|
|
734
|
-
this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
|
|
706
|
+
this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
|
|
735
707
|
}
|
|
736
708
|
}
|
|
737
709
|
|
|
738
710
|
this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
|
|
739
711
|
|
|
740
712
|
// ─── Script-based status detection
|
|
741
|
-
this.scheduleSettle();
|
|
713
|
+
this.engine.scheduleSettle();
|
|
742
714
|
}
|
|
743
715
|
|
|
744
716
|
private resolveStartupState(
|
|
@@ -761,6 +733,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
761
733
|
if (stableMs < 2000) return;
|
|
762
734
|
|
|
763
735
|
const startupModal = this.runParseApproval(this.recentOutputBuffer);
|
|
736
|
+
const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
737
|
+
if (!startupModal && startupStatus !== 'idle') {
|
|
738
|
+
this.scheduleStartupSettleCheck();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
764
741
|
this.startupParseGate = false;
|
|
765
742
|
if (this.startupSettleTimer) {
|
|
766
743
|
clearTimeout(this.startupSettleTimer);
|
|
@@ -768,20 +745,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
768
745
|
}
|
|
769
746
|
this.ready = true;
|
|
770
747
|
if (startupModal) {
|
|
771
|
-
this.activeModal = startupModal;
|
|
772
|
-
this.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
748
|
+
this.engine.activeModal = startupModal;
|
|
749
|
+
this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
773
750
|
} else {
|
|
774
|
-
if (this.currentStatus === 'waiting_approval' || this.activeModal) {
|
|
775
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
751
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
|
|
752
|
+
this.engine.lastApprovalResolvedAt = Date.now();
|
|
776
753
|
}
|
|
777
|
-
this.activeModal = null;
|
|
778
|
-
|
|
754
|
+
this.engine.activeModal = null;
|
|
755
|
+
// Clear the in-flight turn flag at the same time we declare
|
|
756
|
+
// startup-idle. Otherwise the next settled evaluation sees
|
|
757
|
+
// isWaitingForResponse=true + recent CLI welcome-screen paints
|
|
758
|
+
// and flips us right back to generating via the hold path
|
|
759
|
+
// (the "startup → generating → idle → generating → idle"
|
|
760
|
+
// flicker the user observed on claude-cli launch).
|
|
761
|
+
this.engine.isWaitingForResponse = false;
|
|
762
|
+
this.engine.currentTurnScope = null;
|
|
763
|
+
this.engine.setStatus('idle', `startup_ready:${trigger}`);
|
|
779
764
|
}
|
|
780
765
|
LOG.info(
|
|
781
766
|
'CLI',
|
|
782
767
|
`[${this.cliType}] Startup settled (${trigger}, stableMs=${stableMs}, modal=${!!startupModal}) providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
|
|
783
768
|
);
|
|
784
769
|
this.onStatusChange?.();
|
|
770
|
+
// Readiness barrier flush: a message queued because the session was not yet
|
|
771
|
+
// ready (sendMessageNow's not_ready_pending_prompt path) has no turn-completion
|
|
772
|
+
// event to trigger its flush. Now that the interactive prompt is up and we are
|
|
773
|
+
// idle, drain it. No-op when the queue is empty or we settled to a modal.
|
|
774
|
+
if (!startupModal) this.schedulePendingOutboundFlush();
|
|
785
775
|
}
|
|
786
776
|
|
|
787
777
|
private scheduleStartupSettleCheck(): void {
|
|
@@ -801,88 +791,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
801
791
|
}, delayMs);
|
|
802
792
|
}
|
|
803
793
|
|
|
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
794
|
private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
|
|
887
795
|
const startedAt = Date.now();
|
|
888
796
|
let loggedWait = false;
|
|
@@ -892,7 +800,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
892
800
|
const screenText = this.terminalScreen.getText() || '';
|
|
893
801
|
const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
|
|
894
802
|
const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
895
|
-
const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
803
|
+
const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
|
|
896
804
|
const interactiveReady = status === 'idle'
|
|
897
805
|
&& stableMs >= 700
|
|
898
806
|
&& recentlyOutput >= 350;
|
|
@@ -926,637 +834,84 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
926
834
|
|
|
927
835
|
private clearAllTimers(): void {
|
|
928
836
|
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
837
|
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
838
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
936
839
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
840
|
+
this.engine.clearAllTimers();
|
|
937
841
|
}
|
|
938
842
|
|
|
939
|
-
|
|
940
|
-
const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
|
|
941
|
-
const isIdle = this.runDetectStatus(this.recentOutputBuffer) === 'idle';
|
|
942
|
-
if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !isIdle || !!blockingModal) {
|
|
943
|
-
return false;
|
|
944
|
-
}
|
|
945
|
-
this.clearAllTimers();
|
|
946
|
-
this.clearIdleFinishCandidate(reason);
|
|
947
|
-
this.responseBuffer = '';
|
|
948
|
-
this.isWaitingForResponse = false;
|
|
949
|
-
this.responseSettleIgnoreUntil = 0;
|
|
950
|
-
this.submitRetryUsed = false;
|
|
951
|
-
this.submitRetryPromptSnippet = '';
|
|
952
|
-
this.finishRetryCount = 0;
|
|
953
|
-
this.currentTurnScope = null;
|
|
954
|
-
this.activeModal = null;
|
|
955
|
-
this.recordTrace('stale_idle_response_cleared', { reason });
|
|
956
|
-
return true;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
|
|
960
|
-
const raw = String(this.responseBuffer || '').trim();
|
|
961
|
-
if (!raw) return false;
|
|
962
|
-
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
963
|
-
if (!normalizedPrompt) return true;
|
|
964
|
-
const normalizedBuffer = compactPromptText(raw);
|
|
965
|
-
if (!normalizedBuffer) return false;
|
|
966
|
-
if (normalizedBuffer === normalizedPrompt) return false;
|
|
967
|
-
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
968
|
-
const remainder = normalizedBuffer
|
|
969
|
-
.slice(normalizedPrompt.length)
|
|
970
|
-
.replace(/[─═\-]+/g, '')
|
|
971
|
-
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
972
|
-
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
973
|
-
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
974
|
-
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
975
|
-
.replace(/esctointerrupt/gi, '')
|
|
976
|
-
.replace(/❯/g, '')
|
|
977
|
-
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
978
|
-
.trim();
|
|
979
|
-
return remainder.length > 0;
|
|
980
|
-
}
|
|
981
|
-
return true;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
private evaluateSettled(): void {
|
|
985
|
-
const now = Date.now();
|
|
986
|
-
if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
|
|
987
|
-
const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
|
|
988
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
989
|
-
this.settleTimer = setTimeout(() => {
|
|
990
|
-
this.settleTimer = null;
|
|
991
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
992
|
-
this.evaluateSettled();
|
|
993
|
-
}, delayTime);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
this.resolveStartupState('settled');
|
|
998
|
-
if (this.startupParseGate) return;
|
|
999
|
-
|
|
1000
|
-
if (!this.isWaitingForResponse && !this.currentTurnScope && !this.activeModal && !this.parseErrorMessage) {
|
|
1001
|
-
const tail = this.settledBuffer || this.recentOutputBuffer;
|
|
1002
|
-
const modal = this.runParseApproval(tail);
|
|
1003
|
-
const lightweightStatus = this.cliScripts?.detectStatus
|
|
1004
|
-
? this.runDetectStatus(tail)
|
|
1005
|
-
: null;
|
|
1006
|
-
if (!modal && lightweightStatus === 'idle' && this.currentStatus === 'idle') {
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
const session = this.runParseSession();
|
|
1012
|
-
if (!session) return;
|
|
1013
|
-
|
|
1014
|
-
const { status, messages, parsedStatus } = session;
|
|
1015
|
-
const modal = (session as any).activeModal ?? session.modal ?? null;
|
|
1016
|
-
const parsedMessages = normalizeCliParsedMessages(messages, {
|
|
1017
|
-
scope: null,
|
|
1018
|
-
lastOutputAt: this.lastOutputAt,
|
|
1019
|
-
});
|
|
843
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
1020
844
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
responseBuffer: this.responseBuffer,
|
|
1040
|
-
partialResponse: this.responseBuffer,
|
|
1041
|
-
scope: this.currentTurnScope,
|
|
1042
|
-
}),
|
|
845
|
+
runParseSession(): ParsedSession | null {
|
|
846
|
+
const screenText = this.terminalScreen.getText();
|
|
847
|
+
const parseScreenText = this.getParseScreenText(screenText);
|
|
848
|
+
const tail = this.recentOutputBuffer.slice(-500);
|
|
849
|
+
const input = buildCliParseInput({
|
|
850
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
851
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
852
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
853
|
+
terminalScreenText: parseScreenText,
|
|
854
|
+
workingDir: this.workingDir,
|
|
855
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
856
|
+
historySessionId: this.providerSessionId || undefined,
|
|
857
|
+
baseMessages: [],
|
|
858
|
+
partialResponse: this.responseBuffer,
|
|
859
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
860
|
+
scope: this.engine.currentTurnScope,
|
|
861
|
+
runtimeSettings: this.runtimeSettings,
|
|
862
|
+
spawnAt: this.spawnAt,
|
|
1043
863
|
});
|
|
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
|
-
}),
|
|
864
|
+
const session = this.runner.parseSession({
|
|
865
|
+
...input,
|
|
866
|
+
tail,
|
|
867
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1168
868
|
});
|
|
1169
|
-
this.
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
private applyWaitingApproval(ctx: SettledEvalContext): void {
|
|
1173
|
-
const { modal } = ctx;
|
|
1174
|
-
this.clearIdleFinishCandidate('waiting_approval');
|
|
1175
|
-
const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
|
|
1176
|
-
if (inCooldown && !modal) {
|
|
1177
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1178
|
-
this.activeModal = null;
|
|
1179
|
-
if (this.isWaitingForResponse) {
|
|
1180
|
-
this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
|
|
1181
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1182
|
-
this.idleTimeout = setTimeout(() => {
|
|
1183
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1184
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1185
|
-
this.finishResponse();
|
|
1186
|
-
}
|
|
1187
|
-
}, this.timeouts.generatingIdle);
|
|
1188
|
-
} else {
|
|
1189
|
-
this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
|
|
1190
|
-
}
|
|
1191
|
-
this.onStatusChange?.();
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
if (!inCooldown) {
|
|
1195
|
-
if (!modal) {
|
|
1196
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
this.isWaitingForResponse = true;
|
|
1200
|
-
this.setStatus('waiting_approval', 'script_detect');
|
|
1201
|
-
this.activeModal = modal;
|
|
1202
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1203
|
-
this.armApprovalExitTimeout();
|
|
1204
|
-
this.onStatusChange?.();
|
|
1205
|
-
}
|
|
869
|
+
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
870
|
+
return session;
|
|
1206
871
|
}
|
|
1207
872
|
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (prevStatus === 'waiting_approval') {
|
|
1222
|
-
// Transitioned out of approval → generating
|
|
1223
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1224
|
-
this.activeModal = null;
|
|
1225
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1226
|
-
}
|
|
1227
|
-
if (!this.isWaitingForResponse) {
|
|
1228
|
-
this.isWaitingForResponse = true;
|
|
1229
|
-
this.responseBuffer = '';
|
|
1230
|
-
}
|
|
1231
|
-
this.setStatus('generating', 'script_detect');
|
|
1232
|
-
// Reset idle timeout
|
|
1233
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1234
|
-
this.idleTimeout = setTimeout(() => {
|
|
1235
|
-
if (this.isWaitingForResponse) {
|
|
1236
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1237
|
-
this.finishResponse();
|
|
1238
|
-
}
|
|
1239
|
-
}, this.timeouts.generatingIdle);
|
|
1240
|
-
this.onStatusChange?.();
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
private applyIdle(ctx: SettledEvalContext, now: number): void {
|
|
1244
|
-
const { modal, lastParsedAssistant, prevStatus } = ctx;
|
|
1245
|
-
if (prevStatus === 'waiting_approval') {
|
|
1246
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1247
|
-
this.activeModal = null;
|
|
1248
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1249
|
-
this.setStatus('idle', 'approval_prompt_gone_script_idle');
|
|
1250
|
-
}
|
|
1251
|
-
if (!this.isWaitingForResponse) {
|
|
1252
|
-
if (prevStatus !== 'idle') {
|
|
1253
|
-
this.clearIdleFinishCandidate('idle_without_response');
|
|
1254
|
-
this.setStatus('idle', 'script_detect');
|
|
1255
|
-
this.onStatusChange?.();
|
|
1256
|
-
}
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
1260
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
1261
|
-
const hasAssistantTurn = !!lastParsedAssistant;
|
|
1262
|
-
const assistantLength = lastParsedAssistant?.content?.length || 0;
|
|
1263
|
-
const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
|
|
1264
|
-
const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
|
|
1265
|
-
const idleReady = !modal
|
|
1266
|
-
&& hasAssistantTurn
|
|
1267
|
-
&& quietForMs >= idleQuietThresholdMs
|
|
1268
|
-
&& screenStableMs >= idleFinishConfirmMs;
|
|
1269
|
-
const candidate = this.idleFinishCandidate;
|
|
1270
|
-
const candidateQuiet = !!candidate
|
|
1271
|
-
&& candidate.responseEpoch === this.responseEpoch
|
|
1272
|
-
&& candidate.lastOutputAt === this.lastOutputAt
|
|
1273
|
-
&& candidate.lastScreenChangeAt === this.lastScreenChangeAt
|
|
1274
|
-
&& assistantLength >= candidate.assistantLength
|
|
1275
|
-
&& (now - candidate.armedAt) >= idleFinishConfirmMs;
|
|
1276
|
-
|
|
1277
|
-
this.recordTrace('idle_decision', {
|
|
1278
|
-
quietForMs,
|
|
1279
|
-
screenStableMs,
|
|
1280
|
-
hasAssistantTurn,
|
|
1281
|
-
assistantLength,
|
|
1282
|
-
hasModal: !!modal,
|
|
1283
|
-
idleQuietThresholdMs,
|
|
1284
|
-
idleStableThresholdMs: idleFinishConfirmMs,
|
|
1285
|
-
idleReady,
|
|
1286
|
-
idleFinishConfirmMs,
|
|
1287
|
-
idleFinishCandidate: candidate,
|
|
1288
|
-
candidateQuiet,
|
|
1289
|
-
canFinishImmediately: idleReady && candidateQuiet,
|
|
1290
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
1291
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1292
|
-
...buildCliTraceParseSnapshot({
|
|
1293
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1294
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1295
|
-
responseBuffer: this.responseBuffer,
|
|
1296
|
-
partialResponse: this.responseBuffer,
|
|
1297
|
-
scope: this.currentTurnScope,
|
|
1298
|
-
}),
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
if (idleReady && candidateQuiet) {
|
|
1302
|
-
this.clearIdleFinishCandidate('finish_response');
|
|
1303
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1304
|
-
this.finishResponse();
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (idleReady) {
|
|
1309
|
-
if (!candidate) {
|
|
1310
|
-
this.armIdleFinishCandidate(assistantLength);
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
} else {
|
|
1314
|
-
this.clearIdleFinishCandidate('idle_not_ready');
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1318
|
-
this.idleTimeout = setTimeout(() => {
|
|
1319
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1320
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1321
|
-
this.clearIdleFinishCandidate('idle_timeout_finish');
|
|
1322
|
-
this.finishResponse();
|
|
1323
|
-
}
|
|
1324
|
-
}, this.timeouts.idleFinish);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
private finishResponse(): void {
|
|
1328
|
-
if (this.submitPendingUntil > Date.now()) return;
|
|
1329
|
-
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
1330
|
-
this.clearIdleFinishCandidate('finish_response_enter');
|
|
1331
|
-
this.recordTrace('finish_response', {
|
|
1332
|
-
...buildCliTraceParseSnapshot({
|
|
1333
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1334
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1335
|
-
responseBuffer: this.responseBuffer,
|
|
1336
|
-
partialResponse: this.responseBuffer,
|
|
1337
|
-
scope: this.currentTurnScope,
|
|
1338
|
-
}),
|
|
873
|
+
runDetectStatus(text: string): string | null {
|
|
874
|
+
const screenText = this.terminalScreen.getText();
|
|
875
|
+
const tail = text.slice(-500);
|
|
876
|
+
return this.runner.detectStatus({
|
|
877
|
+
tail,
|
|
878
|
+
screenText,
|
|
879
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
880
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
881
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
882
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1339
883
|
});
|
|
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
884
|
}
|
|
1376
885
|
|
|
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),
|
|
886
|
+
runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
887
|
+
const screenText = this.terminalScreen.getText();
|
|
888
|
+
const buffer = screenText || this.accumulatedBuffer;
|
|
889
|
+
return this.runner.parseApproval({
|
|
890
|
+
buffer,
|
|
891
|
+
screenText,
|
|
892
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
893
|
+
tail,
|
|
894
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
895
|
+
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
896
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1408
897
|
});
|
|
1409
|
-
return true;
|
|
1410
898
|
}
|
|
1411
899
|
|
|
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;
|
|
900
|
+
private applyParsedSessionMetadata(parsed: any): void {
|
|
901
|
+
const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
|
|
902
|
+
? parsed.providerSessionId.trim()
|
|
903
|
+
: '';
|
|
904
|
+
if (providerSessionId && providerSessionId !== this.providerSessionId) {
|
|
905
|
+
this.providerSessionId = providerSessionId;
|
|
906
|
+
this.updateRuntimeMeta({ providerSessionId });
|
|
1548
907
|
}
|
|
1549
908
|
}
|
|
1550
909
|
|
|
1551
|
-
private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
|
|
1552
|
-
return !!(startupModal || this.activeModal);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
910
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1556
911
|
if (this.parseErrorMessage) return 'error';
|
|
1557
|
-
if (this.
|
|
1558
|
-
if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
|
|
1559
|
-
return this.currentStatus;
|
|
912
|
+
if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
|
|
913
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
|
|
914
|
+
return this.engine.currentStatus;
|
|
1560
915
|
}
|
|
1561
916
|
|
|
1562
917
|
// ─── Public API (CliAdapter) ───────────────────
|
|
@@ -1564,17 +919,73 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1564
919
|
getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
|
|
1565
920
|
const allowParse = options.allowParse !== false;
|
|
1566
921
|
const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
922
|
+
const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
|
|
923
|
+
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
924
|
+
: null;
|
|
1567
925
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1568
|
-
let effectiveModal = startupModal || this.activeModal;
|
|
926
|
+
let effectiveModal = startupModal || this.engine.activeModal;
|
|
927
|
+
// (fix) When we have no captured modal yet, take one more live attempt
|
|
928
|
+
// with the current screen text — the engine's settle pass can miss
|
|
929
|
+
// the modal when it happens to fire exactly between writes, and
|
|
930
|
+
// without a modal here the dashboard could never show the buttons.
|
|
931
|
+
// We deliberately do NOT overwrite an existing engine.activeModal so
|
|
932
|
+
// a stable matched modal wins. This runs even outside the startup
|
|
933
|
+
// gate because Claude's approval frames can appear long after launch.
|
|
934
|
+
if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
|
|
935
|
+
const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
|
|
936
|
+
if (liveDetect === 'waiting_approval') {
|
|
937
|
+
const liveModal = this.runParseApproval(this.terminalScreen.getText())
|
|
938
|
+
|| this.runParseApproval(this.recentOutputBuffer);
|
|
939
|
+
// (fix) Only surface modals that have at least one non-empty
|
|
940
|
+
// button. Half-rendered approval frames produced
|
|
941
|
+
// { message, buttons: [] } briefly — auto-approve would then
|
|
942
|
+
// pick buttonIndex=-1 and resolveModal would write the
|
|
943
|
+
// provider's index-0 key into the prompt. Skip those frames
|
|
944
|
+
// and let a later evaluate find the complete modal.
|
|
945
|
+
const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
|
|
946
|
+
&& liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
|
|
947
|
+
if (liveModal && buttonsOk) {
|
|
948
|
+
effectiveModal = liveModal;
|
|
949
|
+
if (!this.engine.activeModal) this.engine.activeModal = liveModal;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
// Only surface waiting_approval when we ALSO have a concrete modal
|
|
954
|
+
// (message + buttons). detectStatus alone can fire while parseApproval
|
|
955
|
+
// is still null — the engine logs "detectStatus=waiting_approval but
|
|
956
|
+
// parseApproval returned null; ignoring". Without this guard getStatus
|
|
957
|
+
// was shipping a bare waiting_approval with activeModal=null and the
|
|
958
|
+
// user perceived the flow as broken.
|
|
959
|
+
if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
|
|
960
|
+
effectiveStatus = 'waiting_approval';
|
|
961
|
+
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
962
|
+
effectiveStatus = 'idle';
|
|
963
|
+
}
|
|
1569
964
|
if (allowParse && !startupModal && !effectiveModal) {
|
|
1570
965
|
const parsed = this.getFreshParsedStatusCache();
|
|
1571
966
|
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
1572
967
|
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
1573
968
|
? parsed.activeModal
|
|
1574
969
|
: null;
|
|
970
|
+
const hasFinalAssistant = (p: any) => {
|
|
971
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
972
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
973
|
+
};
|
|
1575
974
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1576
975
|
effectiveStatus = 'waiting_approval';
|
|
1577
976
|
effectiveModal = parsedModal;
|
|
977
|
+
} else if (
|
|
978
|
+
effectiveStatus === 'idle'
|
|
979
|
+
&& parsed?.status === 'generating'
|
|
980
|
+
&& !hasFinalAssistant(parsed)
|
|
981
|
+
) {
|
|
982
|
+
effectiveStatus = 'generating';
|
|
983
|
+
} else if (
|
|
984
|
+
effectiveStatus === 'generating'
|
|
985
|
+
&& parsed?.status === 'idle'
|
|
986
|
+
&& hasFinalAssistant(parsed)
|
|
987
|
+
) {
|
|
988
|
+
effectiveStatus = 'idle';
|
|
1578
989
|
}
|
|
1579
990
|
}
|
|
1580
991
|
const bufferState = this.getBufferState();
|
|
@@ -1583,8 +994,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1583
994
|
messages: [],
|
|
1584
995
|
workingDir: this.workingDir,
|
|
1585
996
|
activeModal: effectiveModal,
|
|
1586
|
-
|
|
1587
|
-
|
|
997
|
+
approvalEntrySeq: this.engine.approvalEntrySeq,
|
|
998
|
+
lastResolvedEntrySeq: this.engine.lastResolvedEntrySeq,
|
|
999
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
1000
|
+
pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
|
|
1001
|
+
id: message.id,
|
|
1002
|
+
role: message.role,
|
|
1003
|
+
content: message.content,
|
|
1004
|
+
queuedAt: message.queuedAt,
|
|
1005
|
+
source: message.source,
|
|
1006
|
+
})),
|
|
1007
|
+
errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
|
|
1008
|
+
errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
|
|
1009
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1010
|
+
lastOutputAt: this.lastOutputAt,
|
|
1011
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1588
1012
|
...(bufferState ? { bufferState } : {}),
|
|
1589
1013
|
};
|
|
1590
1014
|
}
|
|
@@ -1600,15 +1024,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1600
1024
|
const cached = this.parsedStatusCache;
|
|
1601
1025
|
const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
|
|
1602
1026
|
if (
|
|
1603
|
-
|
|
1027
|
+
!this.providerOwnsTranscript()
|
|
1028
|
+
&& cached
|
|
1604
1029
|
&& cached.responseBuffer === this.responseBuffer
|
|
1605
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
1030
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1606
1031
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1607
1032
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1608
1033
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1609
1034
|
&& cached.screenText === parseScreenText
|
|
1610
|
-
&& cached.currentStatus === this.currentStatus
|
|
1611
|
-
&& cached.activeModal === this.activeModal
|
|
1035
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
1036
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1612
1037
|
&& cached.cliName === this.cliName
|
|
1613
1038
|
) {
|
|
1614
1039
|
return cached.result;
|
|
@@ -1623,14 +1048,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1623
1048
|
const bufferState = this.getBufferState();
|
|
1624
1049
|
const result = {
|
|
1625
1050
|
id: (parsed as any).id || 'cli_session',
|
|
1626
|
-
status: parsed.status || this.currentStatus,
|
|
1051
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1627
1052
|
title: (parsed as any).title || this.cliName,
|
|
1628
1053
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1629
1054
|
scope: null,
|
|
1630
1055
|
lastOutputAt: this.lastOutputAt,
|
|
1631
1056
|
}),
|
|
1632
1057
|
activeModal,
|
|
1633
|
-
providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
|
|
1058
|
+
providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
|
|
1059
|
+
errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
|
|
1060
|
+
? (parsed as any).errorMessage.trim()
|
|
1061
|
+
: undefined,
|
|
1062
|
+
errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
|
|
1063
|
+
? (parsed as any).errorReason.trim()
|
|
1064
|
+
: undefined,
|
|
1634
1065
|
...(bufferState ? { bufferState } : {}),
|
|
1635
1066
|
...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
|
|
1636
1067
|
? { transcriptAuthority: (parsed as any).transcriptAuthority }
|
|
@@ -1642,13 +1073,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1642
1073
|
|
|
1643
1074
|
this.parsedStatusCache = {
|
|
1644
1075
|
responseBuffer: this.responseBuffer,
|
|
1645
|
-
currentTurnScope: this.currentTurnScope,
|
|
1076
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1646
1077
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1647
1078
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1648
1079
|
accumulatedRawBufferKey,
|
|
1649
1080
|
screenText: parseScreenText,
|
|
1650
|
-
currentStatus: this.currentStatus,
|
|
1651
|
-
activeModal: this.activeModal,
|
|
1081
|
+
currentStatus: this.engine.currentStatus,
|
|
1082
|
+
activeModal: this.engine.activeModal,
|
|
1652
1083
|
cliName: this.cliName,
|
|
1653
1084
|
result,
|
|
1654
1085
|
};
|
|
@@ -1656,22 +1087,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1656
1087
|
}
|
|
1657
1088
|
|
|
1658
1089
|
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
1090
|
const input = buildCliParseInput({
|
|
1664
1091
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1665
1092
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1666
1093
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1667
1094
|
terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
|
|
1095
|
+
workingDir: this.workingDir,
|
|
1096
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1097
|
+
historySessionId: this.providerSessionId || undefined,
|
|
1668
1098
|
baseMessages: [],
|
|
1669
1099
|
partialResponse: this.responseBuffer,
|
|
1670
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1671
|
-
scope: this.currentTurnScope,
|
|
1100
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1101
|
+
scope: this.engine.currentTurnScope,
|
|
1672
1102
|
runtimeSettings: this.runtimeSettings,
|
|
1103
|
+
spawnAt: this.spawnAt,
|
|
1673
1104
|
});
|
|
1674
|
-
return await Promise.resolve(this.
|
|
1105
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1675
1106
|
...input,
|
|
1676
1107
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1677
1108
|
}));
|
|
@@ -1683,7 +1114,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1683
1114
|
|
|
1684
1115
|
/** Whether this adapter has CLI scripts loaded */
|
|
1685
1116
|
hasCliScripts(): boolean {
|
|
1686
|
-
return
|
|
1117
|
+
return this.runner.hasDetectStatus();
|
|
1687
1118
|
}
|
|
1688
1119
|
|
|
1689
1120
|
/**
|
|
@@ -1692,24 +1123,29 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1692
1123
|
*/
|
|
1693
1124
|
async resolveAction(data: any): Promise<void> {
|
|
1694
1125
|
let promptText = '';
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1126
|
+
try {
|
|
1127
|
+
promptText = this.runner.invokeByName('resolveAction', data);
|
|
1128
|
+
} catch {
|
|
1129
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
|
|
1130
|
+
return;
|
|
1701
1131
|
}
|
|
1702
1132
|
if (!promptText) {
|
|
1703
|
-
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not
|
|
1133
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
|
|
1704
1134
|
return;
|
|
1705
1135
|
}
|
|
1706
1136
|
await this.sendMessage(promptText);
|
|
1707
1137
|
}
|
|
1708
1138
|
|
|
1139
|
+
async setInteractivePromptResponse(_response: InteractivePromptResponse): Promise<void> {
|
|
1140
|
+
// Legacy TUI providers do not currently expose an interactive prompt
|
|
1141
|
+
// protocol. Spec-backed claude-cli implements the first real wiring.
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1709
1144
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
1710
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
1711
|
-
if (this.hasActionableApproval()) return false;
|
|
1712
|
-
|
|
1145
|
+
if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
|
|
1146
|
+
if (this.engine.hasActionableApproval()) return false;
|
|
1147
|
+
// If there's already meaningful response content beyond the echoed prompt, not stuck
|
|
1148
|
+
if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
|
|
1713
1149
|
const screenText = this.terminalScreen.getText();
|
|
1714
1150
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
1715
1151
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -1718,25 +1154,82 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1718
1154
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
1719
1155
|
}
|
|
1720
1156
|
|
|
1157
|
+
private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
|
|
1158
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
1159
|
+
if (!raw) return false;
|
|
1160
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1161
|
+
if (!normalizedPrompt) return true;
|
|
1162
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
1163
|
+
if (!normalizedBuffer) return false;
|
|
1164
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1165
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1166
|
+
const remainder = normalizedBuffer
|
|
1167
|
+
.slice(normalizedPrompt.length)
|
|
1168
|
+
.replace(/[─═\-]+/g, '')
|
|
1169
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1170
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1171
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1172
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1173
|
+
.replace(/esctointerrupt/gi, '')
|
|
1174
|
+
.replace(/❯/g, '')
|
|
1175
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1176
|
+
.trim();
|
|
1177
|
+
return remainder.length > 0;
|
|
1178
|
+
}
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1721
1182
|
private async writeToPty(data: string): Promise<void> {
|
|
1722
1183
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1184
|
+
// win32 ConPTY paced write: a single unbounded write beyond ~1KB overflows
|
|
1185
|
+
// the console input pipe and drops LEADING bytes (the "long message gets
|
|
1186
|
+
// truncated, head lost / tail kept" failure). Split a large payload into
|
|
1187
|
+
// bounded, surrogate-safe chunks written with a short gap so the console
|
|
1188
|
+
// reader keeps up. Small payloads (the common case — short prompts, lone
|
|
1189
|
+
// submit keys) still go out in a single write.
|
|
1190
|
+
//
|
|
1191
|
+
// The submit key, when present, is the TAIL of `data` (callers pass
|
|
1192
|
+
// `body + sendKey` for the atomic-submit paths). Because we chunk the
|
|
1193
|
+
// combined string, the submit key always rides in the SAME final write as
|
|
1194
|
+
// the body's tail — the win32 invariant that ConPTY recognizes it as a
|
|
1195
|
+
// submit — and is never emitted before the whole body has been written
|
|
1196
|
+
// (no partial-body submit). Body-only writes (wait_for_echo strategy) have
|
|
1197
|
+
// their submit key sent separately by the caller afterwards, unchanged.
|
|
1198
|
+
if (process.platform === 'win32' && shouldChunkWin32Write(data.length)) {
|
|
1199
|
+
await this.writeWin32Chunked(data);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1723
1202
|
await this.ptyProcess.write(data);
|
|
1724
1203
|
}
|
|
1725
1204
|
|
|
1205
|
+
/** Write `data` to the PTY in bounded, surrogate-safe chunks with a short
|
|
1206
|
+
* inter-chunk gap (win32 paced write). Awaits each chunk's write and the gap
|
|
1207
|
+
* so the returned promise resolves only after the FINAL chunk (carrying any
|
|
1208
|
+
* trailing submit key) has been written. */
|
|
1209
|
+
private async writeWin32Chunked(data: string): Promise<void> {
|
|
1210
|
+
const chunks = chunkPreservingSurrogates(data, WIN32_PTY_WRITE_CHUNK_CHARS);
|
|
1211
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
1212
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1213
|
+
await this.ptyProcess.write(chunks[i]);
|
|
1214
|
+
if (i + 1 < chunks.length) {
|
|
1215
|
+
await new Promise<void>(resolve => setTimeout(resolve, WIN32_PTY_WRITE_CHUNK_GAP_MS));
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1726
1220
|
private resetPendingSendState(reason: string): void {
|
|
1727
|
-
this.isWaitingForResponse = false;
|
|
1728
1221
|
this.responseBuffer = '';
|
|
1729
|
-
this.currentTurnScope = null;
|
|
1730
|
-
this.submitPendingUntil = 0;
|
|
1731
|
-
this.clearIdleFinishCandidate(reason);
|
|
1732
1222
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1733
|
-
|
|
1734
|
-
|
|
1223
|
+
this.engine.resetActiveTurnState();
|
|
1224
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
1735
1225
|
}
|
|
1736
1226
|
|
|
1737
1227
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
1738
1228
|
if (state.didCommitUserTurn) return;
|
|
1739
1229
|
state.didCommitUserTurn = true;
|
|
1230
|
+
// The first turn has now been written atomically (win32-safe); later turns
|
|
1231
|
+
// can use the normal echo-gated path now that the input layer is proven live.
|
|
1232
|
+
this.firstTurnSent = true;
|
|
1740
1233
|
}
|
|
1741
1234
|
|
|
1742
1235
|
private armResponseTimeout(): void {
|
|
@@ -1748,42 +1241,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1748
1241
|
}
|
|
1749
1242
|
this.responseTimeout = setTimeout(() => {
|
|
1750
1243
|
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
|
-
});
|
|
1244
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
1768
1245
|
|
|
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
|
|
1246
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
1773
1247
|
// normal settled parser instead and keep the turn open unless the provider
|
|
1774
1248
|
// 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
|
-
});
|
|
1249
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1250
|
+
|
|
1251
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
1787
1252
|
this.armResponseTimeout();
|
|
1788
1253
|
}
|
|
1789
1254
|
}, timeoutMs);
|
|
@@ -1795,37 +1260,42 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1795
1260
|
});
|
|
1796
1261
|
}
|
|
1797
1262
|
|
|
1263
|
+
// WIN32-READY-HOLD: choose the retry write for a stuck prompt. When the FIRST turn
|
|
1264
|
+
// is stuck on win32 — the premature-ready swallow window — the prompt text itself
|
|
1265
|
+
// may have been partially eaten by a not-yet-live ConPTY input layer, so re-sending
|
|
1266
|
+
// a bare CR keeps hitting nothing. Re-type the whole `text + sendKey` atomically
|
|
1267
|
+
// once so the input layer (now live) receives a self-contained, submit-coupled
|
|
1268
|
+
// write. All other cases keep the cheap bare-CR retry (the prompt is fully echoed
|
|
1269
|
+
// and only the Enter is missing).
|
|
1270
|
+
private writeStuckRetry(state: SendMessageState, mode: string): void {
|
|
1271
|
+
const retypeFirstTurn = process.platform === 'win32' && state.isFirstTurn;
|
|
1272
|
+
if (retypeFirstTurn) {
|
|
1273
|
+
LOG.info('CLI', `[${this.cliType}] ${mode}: re-typing full prompt atomically (win32 first-turn swallow recovery)`);
|
|
1274
|
+
void this.writeToPty(state.text + this.sendKey).catch((error) => {
|
|
1275
|
+
LOG.warn('CLI', `[${this.cliType}] ${mode} re-type write failed: ${error?.message || error}`);
|
|
1276
|
+
});
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
this.writeSubmitKeyForRetry(mode);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1798
1282
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
1799
1283
|
this.submitRetryTimer = null;
|
|
1800
1284
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1801
|
-
|
|
1802
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1285
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1803
1286
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
|
|
1804
|
-
this.
|
|
1805
|
-
|
|
1806
|
-
attempt,
|
|
1807
|
-
sendKey: this.sendKey,
|
|
1808
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
1809
|
-
});
|
|
1810
|
-
this.writeSubmitKeyForRetry('submit_retry');
|
|
1811
|
-
if (attempt >= 3) { this.submitRetryUsed = true; return; }
|
|
1287
|
+
this.writeStuckRetry(state, 'submit_retry');
|
|
1288
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
1812
1289
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
1813
1290
|
}
|
|
1814
1291
|
|
|
1815
1292
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
1816
1293
|
this.submitRetryTimer = null;
|
|
1817
1294
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1818
|
-
|
|
1819
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1295
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1820
1296
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
|
|
1821
|
-
this.
|
|
1822
|
-
|
|
1823
|
-
attempt: 1,
|
|
1824
|
-
sendKey: this.sendKey,
|
|
1825
|
-
screenText: summarizeCliTraceText(screenText, 500),
|
|
1826
|
-
});
|
|
1827
|
-
this.writeSubmitKeyForRetry('immediate_retry');
|
|
1828
|
-
this.submitRetryUsed = true;
|
|
1297
|
+
this.writeStuckRetry(state, 'immediate_retry');
|
|
1298
|
+
this.engine.submitRetryUsed = true;
|
|
1829
1299
|
}
|
|
1830
1300
|
|
|
1831
1301
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -1833,13 +1303,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1833
1303
|
completion.resolveOnce();
|
|
1834
1304
|
return;
|
|
1835
1305
|
}
|
|
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
|
-
});
|
|
1306
|
+
this.engine.submitPendingUntil = 0;
|
|
1843
1307
|
void this.writeToPty(this.sendKey).then(() => {
|
|
1844
1308
|
this.commitSendUserTurn(state);
|
|
1845
1309
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -1849,13 +1313,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1849
1313
|
}
|
|
1850
1314
|
|
|
1851
1315
|
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
|
-
});
|
|
1316
|
+
this.engine.submitPendingUntil = 0;
|
|
1859
1317
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
1860
1318
|
this.commitSendUserTurn(state);
|
|
1861
1319
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -1899,7 +1357,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1899
1357
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
1900
1358
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
1901
1359
|
};
|
|
1902
|
-
|
|
1360
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1361
|
+
|
|
1903
1362
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
1904
1363
|
// At this point the prompt text write already completed. Rejecting without
|
|
1905
1364
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -1924,12 +1383,145 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1924
1383
|
), 50);
|
|
1925
1384
|
}
|
|
1926
1385
|
|
|
1927
|
-
async sendMessage(text: string): Promise<void> {
|
|
1386
|
+
async sendMessage(text: string, options: { force?: boolean } = {}): Promise<void> {
|
|
1387
|
+
if (options.force === true) {
|
|
1388
|
+
await this.forceSendMessage(text);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
await this.sendMessageNow(text, true);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
async forceSendMessage(text: string): Promise<void> {
|
|
1395
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1396
|
+
const content = String(text || '');
|
|
1397
|
+
if (!content.trim()) return;
|
|
1398
|
+
// Modal-park guard (defense-in-depth — the primary guard is at the
|
|
1399
|
+
// cli-provider-instance force-forward chokepoint). A force-write writes raw
|
|
1400
|
+
// keystrokes into the PTY, bypassing the busy send-guard. If the session is
|
|
1401
|
+
// parked on a tool-consent modal, the modal's key handler eats those bytes
|
|
1402
|
+
// and silently resolves an approval the user never made. Hold the write and
|
|
1403
|
+
// let the mesh reconcile loop redeliver once the modal is resolved. We only
|
|
1404
|
+
// hold for an actionable approval modal; plain generating is still force-written
|
|
1405
|
+
// (that is the deadlock the force path exists to break).
|
|
1406
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.hasActionableApproval()) {
|
|
1407
|
+
LOG.info('CLI', `[${this.cliType}] force-send held — session parked on approval modal (status=${this.engine.currentStatus})`);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
|
|
1411
|
+
// Settle, then submit atomically. The previous split-write fix
|
|
1412
|
+
// (write text → settle → write a separate '\r') regressed win32:
|
|
1413
|
+
// ConPTY does not treat a lone CR arriving in its own PTY chunk after a
|
|
1414
|
+
// delay as a submit key, so the Enter was swallowed and the prompt sat
|
|
1415
|
+
// typed-but-unsent. Instead, honor a fixed settle gap so the TUI input
|
|
1416
|
+
// handler is in its input-accepting state, then write `content + sendKey`
|
|
1417
|
+
// as ONE PTY write — the same atomic submit the normal `immediate`
|
|
1418
|
+
// strategy uses. Keeping the Enter in the same write unit as the text is
|
|
1419
|
+
// the invariant that makes win32 ConPTY recognize the submit, and it
|
|
1420
|
+
// also makes the original inject↔submit race impossible (the Enter can
|
|
1421
|
+
// never be separated from the text it submits). The settle still guards
|
|
1422
|
+
// the original concern — that a force-dispatch injected into a freshly
|
|
1423
|
+
// idle session lands before the TUI is ready to accept input.
|
|
1424
|
+
await this.waitForForceSubmitSettle();
|
|
1425
|
+
await this.writeToPty(content + this.sendKey);
|
|
1426
|
+
this.onStatusChange?.();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
private async waitForForceSubmitSettle(): Promise<void> {
|
|
1430
|
+
// A fixed minimum gap so the input handler is ready to accept the paste.
|
|
1431
|
+
// We deliberately do NOT echo-gate here: the submit key is written in the
|
|
1432
|
+
// same atomic PTY write as the text (forceSendMessage), so there is no
|
|
1433
|
+
// separate Enter that could race the echo, and on win32 the echo gate
|
|
1434
|
+
// never reliably matched anyway.
|
|
1435
|
+
await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_SETTLE_MS));
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private enqueuePendingOutboundMessage(text: string, reason: string): void {
|
|
1439
|
+
const content = String(text || '');
|
|
1440
|
+
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
1441
|
+
if (duplicate) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const queuedAt = Date.now();
|
|
1445
|
+
const message: PendingOutboundMessage = {
|
|
1446
|
+
id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
|
|
1447
|
+
role: 'user',
|
|
1448
|
+
content,
|
|
1449
|
+
queuedAt,
|
|
1450
|
+
source: 'sendMessage',
|
|
1451
|
+
};
|
|
1452
|
+
this.pendingOutboundQueue.push(message);
|
|
1453
|
+
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
1454
|
+
this.onStatusChange?.();
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
1458
|
+
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
1459
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
1460
|
+
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1461
|
+
? String(parsedStatusBeforeSend.status)
|
|
1462
|
+
: '';
|
|
1463
|
+
const hasFinalAssistant = (p: any) => {
|
|
1464
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1465
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1466
|
+
};
|
|
1467
|
+
if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
|
|
1468
|
+
if (this.engine.currentStatus === 'generating') return 'current_status_generating';
|
|
1469
|
+
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating') {
|
|
1470
|
+
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1471
|
+
const parsedHasActionableModal = Boolean(
|
|
1472
|
+
parsedModal
|
|
1473
|
+
&& Array.isArray(parsedModal.buttons)
|
|
1474
|
+
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1475
|
+
);
|
|
1476
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1477
|
+
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1478
|
+
&& !this.engine.isWaitingForResponse
|
|
1479
|
+
&& !this.engine.currentTurnScope
|
|
1480
|
+
&& !this.engine.hasActionableApproval()
|
|
1481
|
+
&& !parsedHasActionableModal;
|
|
1482
|
+
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
1483
|
+
}
|
|
1484
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
private schedulePendingOutboundFlush(delayMs = 0): void {
|
|
1489
|
+
if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
|
|
1490
|
+
this.pendingOutboundFlushTimer = setTimeout(() => {
|
|
1491
|
+
this.pendingOutboundFlushTimer = null;
|
|
1492
|
+
void this.flushPendingOutboundQueue();
|
|
1493
|
+
}, Math.max(0, delayMs));
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private async flushPendingOutboundQueue(): Promise<void> {
|
|
1497
|
+
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
1498
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
1499
|
+
this.pendingOutboundFlushInFlight = true;
|
|
1500
|
+
try {
|
|
1501
|
+
while (this.pendingOutboundQueue.length > 0) {
|
|
1502
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
1503
|
+
const next = this.pendingOutboundQueue[0];
|
|
1504
|
+
try {
|
|
1505
|
+
await this.sendMessageNow(next.content, false);
|
|
1506
|
+
this.pendingOutboundQueue.shift();
|
|
1507
|
+
this.onStatusChange?.();
|
|
1508
|
+
} catch (error: any) {
|
|
1509
|
+
LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
|
|
1510
|
+
this.schedulePendingOutboundFlush(1000);
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
} finally {
|
|
1515
|
+
this.pendingOutboundFlushInFlight = false;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
|
|
1928
1520
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1929
1521
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
1930
1522
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
1931
|
-
&& this.isWaitingForResponse
|
|
1932
|
-
&& !this.hasActionableApproval();
|
|
1523
|
+
&& this.engine.isWaitingForResponse
|
|
1524
|
+
&& !this.engine.hasActionableApproval();
|
|
1933
1525
|
if (this.startupParseGate) {
|
|
1934
1526
|
const deadline = Date.now() + 10000;
|
|
1935
1527
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -1937,76 +1529,102 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1937
1529
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1938
1530
|
}
|
|
1939
1531
|
}
|
|
1532
|
+
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
1533
|
+
? (() => {
|
|
1534
|
+
try {
|
|
1535
|
+
return this.getScriptParsedStatus?.() || null;
|
|
1536
|
+
} catch {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
})()
|
|
1540
|
+
: null;
|
|
1541
|
+
const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
|
|
1542
|
+
if (allowQueue && queueReason) {
|
|
1543
|
+
this.enqueuePendingOutboundMessage(text, queueReason);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1940
1546
|
if (!allowInterventionPrompt) {
|
|
1941
1547
|
await this.waitForInteractivePrompt();
|
|
1942
1548
|
}
|
|
1943
1549
|
if (!this.ready) {
|
|
1944
1550
|
this.resolveStartupState('send_precheck');
|
|
1945
|
-
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1551
|
+
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
1946
1552
|
this.ready = true;
|
|
1947
1553
|
this.startupParseGate = false;
|
|
1554
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1948
1555
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
1949
1556
|
}
|
|
1950
1557
|
}
|
|
1951
|
-
if (!this.ready)
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1558
|
+
if (!this.ready) {
|
|
1559
|
+
// Readiness barrier (queue-until-ready). A task dispatched the instant a
|
|
1560
|
+
// freshly-spawned session is launched can arrive BEFORE the PTY prints its
|
|
1561
|
+
// interactive prompt (this.ready flips ~2-6s later). Previously this threw
|
|
1562
|
+
// "not ready" and the delegated-task delivery promise requeued the task,
|
|
1563
|
+
// which on win32 raced the auto-launch cooldown and could strand the worker
|
|
1564
|
+
// idle with no work (the "first big message lost" failure). Instead, when the
|
|
1565
|
+
// caller allows queueing, BUFFER the message in the pending-outbound queue and
|
|
1566
|
+
// return — the startup-settle path flips this.ready and flushes the queue once
|
|
1567
|
+
// the prompt is actually up (see resolveStartupState → flushPendingOutboundQueue),
|
|
1568
|
+
// so the message is delivered late rather than dropped. A non-queueable caller
|
|
1569
|
+
// (e.g. an internal flush) still throws so it isn't silently swallowed.
|
|
1570
|
+
if (allowQueue) {
|
|
1571
|
+
this.enqueuePendingOutboundMessage(text, 'not_ready_pending_prompt');
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
1575
|
+
}
|
|
1961
1576
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1962
1577
|
? String(parsedStatusBeforeSend.status)
|
|
1963
1578
|
: '';
|
|
1964
|
-
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating')) {
|
|
1579
|
+
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating')) {
|
|
1965
1580
|
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1966
1581
|
const parsedHasActionableModal = Boolean(
|
|
1967
1582
|
parsedModal
|
|
1968
1583
|
&& Array.isArray(parsedModal.buttons)
|
|
1969
1584
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1970
1585
|
);
|
|
1971
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1586
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1972
1587
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1973
|
-
&& !this.isWaitingForResponse
|
|
1974
|
-
&& !this.currentTurnScope
|
|
1975
|
-
&& !this.hasActionableApproval()
|
|
1588
|
+
&& !this.engine.isWaitingForResponse
|
|
1589
|
+
&& !this.engine.currentTurnScope
|
|
1590
|
+
&& !this.engine.hasActionableApproval()
|
|
1976
1591
|
&& !parsedHasActionableModal;
|
|
1977
1592
|
if (!terminalLooksIdle) {
|
|
1593
|
+
if (allowQueue) {
|
|
1594
|
+
this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1978
1597
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1979
1598
|
}
|
|
1980
1599
|
}
|
|
1981
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1982
|
-
|
|
1600
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1601
|
+
const snap = this.getSnapshot();
|
|
1602
|
+
if (
|
|
1603
|
+
!this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
|
|
1604
|
+
&& !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
|
|
1605
|
+
) {
|
|
1606
|
+
if (allowQueue) {
|
|
1607
|
+
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1983
1610
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1984
1611
|
}
|
|
1985
1612
|
}
|
|
1986
|
-
this.isWaitingForResponse = true;
|
|
1987
1613
|
this.responseBuffer = '';
|
|
1988
|
-
|
|
1989
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1990
|
-
this.clearIdleFinishCandidate('send_message');
|
|
1991
|
-
this.currentTurnScope = {
|
|
1614
|
+
const turnScope: TurnParseScope = {
|
|
1992
1615
|
prompt: text,
|
|
1993
1616
|
startedAt: Date.now(),
|
|
1994
1617
|
bufferStart: this.accumulatedBuffer.length,
|
|
1995
1618
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
1996
1619
|
};
|
|
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);
|
|
1620
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2006
1621
|
if (this.submitRetryTimer) {
|
|
2007
1622
|
clearTimeout(this.submitRetryTimer);
|
|
2008
1623
|
this.submitRetryTimer = null;
|
|
2009
1624
|
}
|
|
1625
|
+
this.engine.onTurnStarted(turnScope);
|
|
1626
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1627
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2010
1628
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2011
1629
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2012
1630
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2018,13 +1636,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2018
1636
|
maxEchoWaitMs,
|
|
2019
1637
|
retryDelayMs,
|
|
2020
1638
|
didCommitUserTurn: false,
|
|
1639
|
+
// Capture BEFORE the send commits — commitSendUserTurn flips firstTurnSent.
|
|
1640
|
+
isFirstTurn: !this.firstTurnSent,
|
|
2021
1641
|
};
|
|
2022
|
-
|
|
2023
|
-
clearTimeout(this.settleTimer);
|
|
2024
|
-
this.settleTimer = null;
|
|
2025
|
-
}
|
|
2026
|
-
this.responseEpoch += 1;
|
|
2027
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1642
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2028
1643
|
await new Promise<void>((resolve, reject) => {
|
|
2029
1644
|
let resolved = false;
|
|
2030
1645
|
const completion: SendMessageCompletion = {
|
|
@@ -2041,30 +1656,34 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2041
1656
|
},
|
|
2042
1657
|
};
|
|
2043
1658
|
|
|
2044
|
-
|
|
1659
|
+
// WIN32-READY-HOLD: the very first turn after startup is the one exposed to
|
|
1660
|
+
// the premature-ready swallow (ready released before win32 ConPTY input is
|
|
1661
|
+
// live). Force it through the atomic content+sendKey single write so the
|
|
1662
|
+
// submit CR can never be separated from the text it submits — the same path
|
|
1663
|
+
// the `immediate` strategy already uses. Restricted to win32 + the first
|
|
1664
|
+
// turn so Mac/linux echo-gated behavior and all later turns are unchanged.
|
|
1665
|
+
const useAtomicFirstTurn = this.submitStrategy === 'immediate'
|
|
1666
|
+
|| (process.platform === 'win32' && sendState.isFirstTurn);
|
|
1667
|
+
if (useAtomicFirstTurn) {
|
|
2045
1668
|
this.submitImmediatePrompt(sendState, completion);
|
|
2046
1669
|
return;
|
|
2047
1670
|
}
|
|
2048
1671
|
|
|
2049
1672
|
if (submitDelayMs > 0) {
|
|
2050
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1673
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2051
1674
|
}
|
|
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
1675
|
const submitStartedAt = Date.now();
|
|
2059
1676
|
void this.writeToPty(text).then(
|
|
2060
1677
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2061
1678
|
completion.rejectOnce,
|
|
2062
1679
|
);
|
|
2063
1680
|
});
|
|
1681
|
+
// Schedule settle after successful send
|
|
1682
|
+
this.engine.scheduleSettle();
|
|
2064
1683
|
}
|
|
2065
1684
|
|
|
2066
1685
|
getPartialResponse(): string {
|
|
2067
|
-
if (!this.isWaitingForResponse) return '';
|
|
1686
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2068
1687
|
return this.responseBuffer;
|
|
2069
1688
|
}
|
|
2070
1689
|
|
|
@@ -2077,10 +1696,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2077
1696
|
cliType: this.cliType,
|
|
2078
1697
|
cliName: this.cliName,
|
|
2079
1698
|
workingDir: this.workingDir,
|
|
2080
|
-
currentStatus: this.currentStatus,
|
|
1699
|
+
currentStatus: this.engine.currentStatus,
|
|
2081
1700
|
ready: this.ready,
|
|
2082
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2083
|
-
activeModal: this.activeModal,
|
|
1701
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1702
|
+
activeModal: this.engine.activeModal,
|
|
2084
1703
|
parseErrorMessage: this.parseErrorMessage,
|
|
2085
1704
|
messageCounts: {
|
|
2086
1705
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2106,10 +1725,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2106
1725
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2107
1726
|
},
|
|
2108
1727
|
parser: {
|
|
2109
|
-
scriptNames:
|
|
2110
|
-
traceSessionId: this.
|
|
2111
|
-
traceSeq: this.
|
|
2112
|
-
currentTurnScope: this.currentTurnScope,
|
|
1728
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1729
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1730
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1731
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2113
1732
|
parsedStatusCache: parsedResult
|
|
2114
1733
|
? {
|
|
2115
1734
|
id: parsedResult.id,
|
|
@@ -2122,26 +1741,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2122
1741
|
activeModal: parsedResult.activeModal,
|
|
2123
1742
|
}
|
|
2124
1743
|
: null,
|
|
2125
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2126
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1744
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1745
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2127
1746
|
},
|
|
2128
1747
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2129
|
-
statusHistory: this.
|
|
2130
|
-
traceEntries: this.
|
|
1748
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1749
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2131
1750
|
timing: {
|
|
2132
1751
|
spawnAt: this.spawnAt,
|
|
2133
1752
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2134
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2135
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2136
|
-
responseEpoch: this.responseEpoch,
|
|
1753
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1754
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1755
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2137
1756
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2138
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1757
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2139
1758
|
},
|
|
2140
1759
|
finish: {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1760
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1761
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1762
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2145
1763
|
},
|
|
2146
1764
|
};
|
|
2147
1765
|
}
|
|
@@ -2151,7 +1769,48 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2151
1769
|
return this.ptyProcess.getMetadata();
|
|
2152
1770
|
}
|
|
2153
1771
|
|
|
1772
|
+
/**
|
|
1773
|
+
* Launch metadata for the dashboard Session info panel. Re-derives the spawn plan
|
|
1774
|
+
* (pure — same inputs the live PTY was spawned with) so the dashboard sees the
|
|
1775
|
+
* resolved binary, full arg vector, and cwd without us having to persist it.
|
|
1776
|
+
* extraEnv values are intentionally dropped (keys only) so secrets passed at
|
|
1777
|
+
* launch time never reach the dashboard.
|
|
1778
|
+
*/
|
|
1779
|
+
getLaunchInfo(): CliLaunchInfo {
|
|
1780
|
+
let command: string | undefined;
|
|
1781
|
+
let args: string[] = [...this.extraArgs];
|
|
1782
|
+
try {
|
|
1783
|
+
const plan = resolveCliSpawnPlan({
|
|
1784
|
+
provider: this.provider,
|
|
1785
|
+
runtimeSettings: this.runtimeSettings,
|
|
1786
|
+
workingDir: this.workingDir,
|
|
1787
|
+
extraArgs: this.extraArgs,
|
|
1788
|
+
extraEnv: this.extraEnv,
|
|
1789
|
+
});
|
|
1790
|
+
command = plan.binaryPath;
|
|
1791
|
+
args = plan.allArgs;
|
|
1792
|
+
} catch {
|
|
1793
|
+
// Spawn plan resolution can throw before the binary is resolvable
|
|
1794
|
+
// (e.g. CLI not installed). Fall back to the raw extra args so the
|
|
1795
|
+
// panel still shows what it can rather than failing the whole call.
|
|
1796
|
+
}
|
|
1797
|
+
return {
|
|
1798
|
+
command,
|
|
1799
|
+
args,
|
|
1800
|
+
extraArgs: [...this.extraArgs],
|
|
1801
|
+
cwd: this.workingDir,
|
|
1802
|
+
extraEnvKeys: Object.keys(this.extraEnv || {}),
|
|
1803
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
|
|
2154
1807
|
updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
|
|
1808
|
+
const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
|
|
1809
|
+
? meta.providerSessionId.trim()
|
|
1810
|
+
: '';
|
|
1811
|
+
if (nextProviderSessionId) {
|
|
1812
|
+
this.providerSessionId = nextProviderSessionId;
|
|
1813
|
+
}
|
|
2155
1814
|
if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
|
|
2156
1815
|
this.ptyProcess.updateMeta(meta, replace);
|
|
2157
1816
|
}
|
|
@@ -2172,7 +1831,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2172
1831
|
this.timeouts.shutdownGrace,
|
|
2173
1832
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2174
1833
|
);
|
|
2175
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1834
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2176
1835
|
|
|
2177
1836
|
try {
|
|
2178
1837
|
if (wasProcessing) {
|
|
@@ -2210,7 +1869,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2210
1869
|
return new Promise((resolve) => {
|
|
2211
1870
|
const startedAt = Date.now();
|
|
2212
1871
|
const timer = setInterval(() => {
|
|
2213
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
1872
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2214
1873
|
clearInterval(timer);
|
|
2215
1874
|
resolve(true);
|
|
2216
1875
|
return;
|
|
@@ -2224,18 +1883,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2224
1883
|
}
|
|
2225
1884
|
|
|
2226
1885
|
shutdown(): void {
|
|
2227
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
1886
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2228
1887
|
this.clearAllTimers();
|
|
2229
1888
|
this.pendingOutputParseChunks = [];
|
|
2230
1889
|
this.pendingTerminalQueryTail = '';
|
|
2231
1890
|
this.ptyOutputChunks = [];
|
|
2232
|
-
this.
|
|
1891
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1892
|
+
this.pendingOutboundQueue = [];
|
|
1893
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2233
1894
|
if (this.ptyProcess) {
|
|
2234
1895
|
this.ptyProcess.write('\x03');
|
|
2235
1896
|
setTimeout(() => {
|
|
2236
1897
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2237
1898
|
this.ptyProcess = null;
|
|
2238
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
1899
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2239
1900
|
this.ready = false;
|
|
2240
1901
|
this.startupParseGate = false;
|
|
2241
1902
|
this.spawnAt = 0;
|
|
@@ -2245,12 +1906,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2245
1906
|
}
|
|
2246
1907
|
|
|
2247
1908
|
detach(): void {
|
|
2248
|
-
this.clearIdleFinishCandidate('detach');
|
|
1909
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2249
1910
|
this.clearAllTimers();
|
|
2250
1911
|
this.pendingOutputParseChunks = [];
|
|
2251
1912
|
this.pendingTerminalQueryTail = '';
|
|
2252
1913
|
this.ptyOutputChunks = [];
|
|
2253
|
-
this.
|
|
1914
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1915
|
+
this.pendingOutboundQueue = [];
|
|
1916
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2254
1917
|
if (this.ptyProcess) {
|
|
2255
1918
|
try {
|
|
2256
1919
|
if (typeof this.ptyProcess.detach === 'function') {
|
|
@@ -2268,82 +1931,128 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2268
1931
|
}
|
|
2269
1932
|
|
|
2270
1933
|
clearHistory(): void {
|
|
2271
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
1934
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2272
1935
|
this.accumulatedBuffer = '';
|
|
2273
1936
|
this.accumulatedRawBuffer = '';
|
|
2274
|
-
this.currentTurnScope = null;
|
|
2275
|
-
this.submitRetryUsed = false;
|
|
2276
|
-
this.submitRetryPromptSnippet = '';
|
|
1937
|
+
this.engine.currentTurnScope = null;
|
|
1938
|
+
this.engine.submitRetryUsed = false;
|
|
1939
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2277
1940
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2278
1941
|
this.pendingOutputParseChunks = [];
|
|
2279
1942
|
this.pendingTerminalQueryTail = '';
|
|
2280
1943
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2281
1944
|
this.ptyOutputChunks = [];
|
|
2282
|
-
if (this.
|
|
2283
|
-
this.
|
|
1945
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
1946
|
+
this.pendingOutboundQueue = [];
|
|
1947
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2284
1948
|
this.resetTerminalScreen();
|
|
2285
1949
|
this.ptyProcess?.clearBuffer?.();
|
|
2286
1950
|
this.onStatusChange?.();
|
|
2287
1951
|
}
|
|
2288
1952
|
|
|
2289
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
1953
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2290
1954
|
isReady(): boolean { return this.ready; }
|
|
2291
1955
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
1956
|
+
// ─── State machine property accessors (delegate to engine) ──────────────
|
|
1957
|
+
// These expose engine state for external callers (tests, debug tools, etc.)
|
|
1958
|
+
|
|
1959
|
+
get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
|
|
1960
|
+
set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
|
|
1961
|
+
|
|
1962
|
+
get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
|
|
1963
|
+
set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
|
|
1964
|
+
|
|
1965
|
+
get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
|
|
1966
|
+
set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
|
|
1967
|
+
|
|
1968
|
+
get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
|
|
1969
|
+
set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
|
|
1970
|
+
|
|
1971
|
+
get responseEpoch(): number { return this.engine.responseEpoch; }
|
|
1972
|
+
set responseEpoch(v: number) { this.engine.responseEpoch = v; }
|
|
1973
|
+
|
|
1974
|
+
get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
|
|
1975
|
+
set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
|
|
1976
|
+
|
|
1977
|
+
get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
|
|
1978
|
+
set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
|
|
1979
|
+
|
|
1980
|
+
get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
|
|
1981
|
+
set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
|
|
1982
|
+
|
|
1983
|
+
get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
|
|
1984
|
+
set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
|
|
1985
|
+
|
|
1986
|
+
get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
|
|
1987
|
+
set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
|
|
1988
|
+
|
|
1989
|
+
get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
|
|
1990
|
+
get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
|
|
1991
|
+
|
|
1992
|
+
get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
|
|
1993
|
+
get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
|
|
1994
|
+
|
|
1995
|
+
get finishRetryCount(): number { return this.engine.finishRetryCount; }
|
|
1996
|
+
set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
|
|
1997
|
+
|
|
1998
|
+
get traceSessionId(): string { return this.engine.getTraceSessionId(); }
|
|
1999
|
+
get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
|
|
2000
|
+
get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
|
|
2001
|
+
get traceSeq(): number { return this.engine.getTraceEntries().length; }
|
|
2002
|
+
|
|
2003
|
+
/** Expose engine's evaluateSettled for test access */
|
|
2004
|
+
evaluateSettled(): void {
|
|
2005
|
+
LOG.debug(
|
|
2006
|
+
'CLI',
|
|
2007
|
+
`[${this.cliType}] settled diagnostics delegated to state engine`);
|
|
2008
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
2009
|
+
}
|
|
2010
|
+
/** Expose engine's scheduleSettle for test access */
|
|
2011
|
+
scheduleSettle(): void { this.engine.scheduleSettle(); }
|
|
2012
|
+
/** Expose engine's clearIdleFinishCandidate for test access */
|
|
2013
|
+
clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
|
|
2014
|
+
/** Expose engine's finishResponse for test access */
|
|
2015
|
+
finishResponse(): void { this.engine.finishResponse(); }
|
|
2016
|
+
/** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
|
|
2017
|
+
getSnapshot(): CliBufferSnapshot {
|
|
2018
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
2019
|
+
return {
|
|
2020
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
2021
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
2022
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
2023
|
+
responseBuffer: this.responseBuffer,
|
|
2024
|
+
screenText,
|
|
2025
|
+
parseScreenText: this.getParseScreenText(screenText),
|
|
2026
|
+
workingDir: this.workingDir,
|
|
2027
|
+
providerSessionId: this.providerSessionId,
|
|
2028
|
+
runtimeSettings: this.runtimeSettings,
|
|
2029
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
2030
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2031
|
+
lastOutputAt: this.lastOutputAt,
|
|
2032
|
+
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2033
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2034
|
+
lastScreenSnapshot: this.lastScreenSnapshot,
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
isAlive(): boolean { return this.ptyProcess !== null; }
|
|
2038
|
+
flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
|
|
2039
|
+
|
|
2040
|
+
async writeRaw(data: string | Buffer): Promise<void> {
|
|
2041
|
+
const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
|
2042
|
+
await this.writeToPty(str);
|
|
2298
2043
|
}
|
|
2299
2044
|
|
|
2300
2045
|
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
|
-
}
|
|
2046
|
+
this.engine.resolveModal(buttonIndex);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
2050
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
2054
|
+
isApprovalRecentlyResolved(): boolean {
|
|
2055
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2347
2056
|
}
|
|
2348
2057
|
|
|
2349
2058
|
resize(cols: number, rows: number): void {
|
|
@@ -2357,7 +2066,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2357
2066
|
}
|
|
2358
2067
|
|
|
2359
2068
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2360
|
-
if (this.startupParseGate ||
|
|
2069
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2361
2070
|
try {
|
|
2362
2071
|
const parsed = this.getScriptParsedStatus();
|
|
2363
2072
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2369,18 +2078,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2369
2078
|
getDebugState(): Record<string, any> {
|
|
2370
2079
|
const screenText = sanitizeTerminalText(this.terminalScreen.getText());
|
|
2371
2080
|
const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
2372
|
-
const
|
|
2373
|
-
|
|
2081
|
+
const startupDetectedStatus = this.startupParseGate && !startupModal
|
|
2082
|
+
? this.runDetectStatus(this.recentOutputBuffer || screenText)
|
|
2083
|
+
: null;
|
|
2084
|
+
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2374
2085
|
const parsedDebugState = this.getParsedDebugState();
|
|
2375
2086
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
2087
|
+
const hasFinalAssistant = (p: any) => {
|
|
2088
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
2089
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
2090
|
+
};
|
|
2091
|
+
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
2092
|
+
if (parsedDebugState?.status === 'error') {
|
|
2093
|
+
effectiveStatus = 'error';
|
|
2094
|
+
}
|
|
2095
|
+
// (fix) Mirror the getStatus contract: never surface waiting_approval
|
|
2096
|
+
// without a concrete modal. detectStatus alone can fire while
|
|
2097
|
+
// parseApproval is still null, and a bare debugState waiting_approval
|
|
2098
|
+
// confused dashboards reading the debug payload.
|
|
2099
|
+
const debugEffectiveModal = startupModal || this.engine.activeModal;
|
|
2100
|
+
if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
|
|
2101
|
+
effectiveStatus = 'waiting_approval';
|
|
2102
|
+
}
|
|
2103
|
+
if (
|
|
2104
|
+
effectiveStatus === 'idle'
|
|
2105
|
+
&& parsedDebugState?.status === 'generating'
|
|
2106
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
2107
|
+
) {
|
|
2108
|
+
effectiveStatus = 'generating';
|
|
2109
|
+
}
|
|
2376
2110
|
return {
|
|
2377
2111
|
type: this.cliType,
|
|
2378
2112
|
name: this.cliName,
|
|
2379
2113
|
providerResolution: this.providerResolutionMeta,
|
|
2380
2114
|
status: effectiveStatus,
|
|
2381
2115
|
projectedStatus: effectiveStatus,
|
|
2382
|
-
rawStatus: this.currentStatus,
|
|
2383
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2116
|
+
rawStatus: this.engine.currentStatus,
|
|
2117
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2384
2118
|
ready: effectiveReady,
|
|
2385
2119
|
startupParseGate: this.startupParseGate,
|
|
2386
2120
|
spawnAt: this.spawnAt,
|
|
@@ -2394,38 +2128,47 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2394
2128
|
providerSessionId: parsedDebugState.providerSessionId,
|
|
2395
2129
|
transcriptAuthority: parsedDebugState.transcriptAuthority,
|
|
2396
2130
|
coverage: parsedDebugState.coverage,
|
|
2131
|
+
errorMessage: parsedDebugState.errorMessage,
|
|
2132
|
+
errorReason: parsedDebugState.errorReason,
|
|
2397
2133
|
activeModal: parsedDebugState.activeModal,
|
|
2398
2134
|
messageCount: parsedMessages.length,
|
|
2399
2135
|
} : null,
|
|
2400
2136
|
screenText: screenText.slice(-4000),
|
|
2401
|
-
currentTurnScope: this.currentTurnScope,
|
|
2137
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2402
2138
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2403
2139
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2404
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2405
2140
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2406
2141
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2407
2142
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
2408
2143
|
sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
|
|
2409
2144
|
responseBuffer: this.responseBuffer.slice(-1000),
|
|
2145
|
+
pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
|
|
2146
|
+
id: message.id,
|
|
2147
|
+
role: message.role,
|
|
2148
|
+
content: message.content,
|
|
2149
|
+
queuedAt: message.queuedAt,
|
|
2150
|
+
source: message.source,
|
|
2151
|
+
})),
|
|
2152
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
2410
2153
|
lastOutputAt: this.lastOutputAt,
|
|
2411
2154
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2412
2155
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2413
2156
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2414
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2415
|
-
activeModal: startupModal || this.activeModal,
|
|
2416
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
2157
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
2158
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
2159
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2417
2160
|
sendDelayMs: this.sendDelayMs,
|
|
2418
2161
|
sendKey: this.sendKey,
|
|
2419
2162
|
submitStrategy: this.submitStrategy,
|
|
2420
2163
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2421
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2422
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2164
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
2165
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2423
2166
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2424
2167
|
hasCliScripts: this.hasCliScripts(),
|
|
2425
|
-
scriptNames:
|
|
2426
|
-
traceSessionId: this.
|
|
2427
|
-
traceEntryCount: this.
|
|
2428
|
-
statusHistory: this.
|
|
2168
|
+
scriptNames: this.runner.getScriptNames(),
|
|
2169
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
2170
|
+
traceEntryCount: this.engine.getTraceEntries().length,
|
|
2171
|
+
statusHistory: this.engine.getStatusHistory().slice(-30),
|
|
2429
2172
|
timeouts: this.timeouts,
|
|
2430
2173
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2431
2174
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2435,20 +2178,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2435
2178
|
|
|
2436
2179
|
getTraceState(limit = 120): Record<string, any> {
|
|
2437
2180
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
2181
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2438
2182
|
return {
|
|
2439
|
-
sessionId: this.
|
|
2183
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2440
2184
|
providerResolution: this.providerResolutionMeta,
|
|
2441
|
-
entryCount:
|
|
2442
|
-
entries:
|
|
2185
|
+
entryCount: traceEntries.length,
|
|
2186
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2443
2187
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2444
2188
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2445
2189
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2446
2190
|
status: this.projectEffectiveStatus(),
|
|
2447
2191
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2448
|
-
rawStatus: this.currentStatus,
|
|
2449
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2450
|
-
activeModal: this.activeModal,
|
|
2451
|
-
currentTurnScope: this.currentTurnScope,
|
|
2192
|
+
rawStatus: this.engine.currentStatus,
|
|
2193
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2194
|
+
activeModal: this.engine.activeModal,
|
|
2195
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2452
2196
|
messages: [],
|
|
2453
2197
|
};
|
|
2454
2198
|
}
|