@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.490
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 +68 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +220 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +137 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +5 -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 +69 -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 +23 -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 +71 -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 +64 -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 +48 -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 +152 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router-aggregate-status.d.ts +24 -0
- package/dist/commands/router-mesh-session-owner.d.ts +59 -0
- package/dist/commands/router-refine.d.ts +151 -0
- package/dist/commands/router-worktree-cleanup.d.ts +133 -0
- package/dist/commands/router.d.ts +203 -15
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +11 -0
- package/dist/config/config.d.ts +19 -0
- package/dist/config/mesh-config.d.ts +65 -0
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/registry-resolver.d.ts +54 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/config/state-store.d.ts +30 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/cli-detector.d.ts +17 -0
- 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 +26 -2
- 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 +80 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/git-worktree.d.ts +71 -1
- package/dist/git/index.d.ts +3 -1
- package/dist/index.d.ts +70 -17
- package/dist/index.js +53532 -19073
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +53283 -18989
- 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/debug-config.d.ts +16 -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 +256 -0
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +203 -0
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
- package/dist/mesh/mesh-event-classify.d.ts +7 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +58 -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 +258 -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 +21 -1
- package/dist/mesh/mesh-init.d.ts +128 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
- package/dist/mesh/mesh-ledger.d.ts +139 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-missions.d.ts +281 -0
- package/dist/mesh/mesh-node-identity.d.ts +310 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
- package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
- package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
- package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +457 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +15 -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 +567 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
- package/dist/mesh/mesh-task-inflight.d.ts +46 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +392 -1
- package/dist/mesh/preview-freshness.d.ts +28 -0
- package/dist/mesh/refine-config.d.ts +97 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +46 -0
- package/dist/providers/chat-message-normalization.d.ts +58 -1
- package/dist/providers/cli-provider-effect-format.d.ts +30 -0
- package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
- package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
- package/dist/providers/cli-provider-instance-types.d.ts +45 -0
- package/dist/providers/cli-provider-instance.d.ts +426 -30
- package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
- package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
- package/dist/providers/contracts.d.ts +196 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +79 -0
- package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -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 +44 -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 +30 -0
- package/dist/providers/provider-instance.d.ts +31 -1
- package/dist/providers/provider-loader.d.ts +42 -5
- 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 +98 -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 +449 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +202 -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 +244 -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 +616 -12
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/session-host/managed-host.d.ts +64 -0
- package/dist/session-host/runtime-surface.d.ts +10 -16
- package/dist/sessions/registry.d.ts +26 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +110 -55
- package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
- 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 +27 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +20 -6
- package/package.json +11 -4
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +80 -14
- 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 +3 -1
- 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 +1282 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +1048 -1143
- package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/src/cli-adapters/provider-cli-parse.ts +19 -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 +204 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2788 -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 +891 -0
- package/src/commands/chat-commands.ts +19 -1841
- package/src/commands/cli-manager.d.ts +2 -0
- package/src/commands/cli-manager.ts +724 -27
- package/src/commands/handler.ts +865 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
- package/src/commands/high-family/mesh-events.ts +102 -0
- package/src/commands/high-family/mesh-status.ts +857 -0
- package/src/commands/high-family/types.ts +80 -0
- package/src/commands/low-family/coordinator-prompt.ts +125 -0
- package/src/commands/low-family/daemon-lifecycle.ts +120 -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 +282 -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 +270 -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 +1268 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +167 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +153 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router-aggregate-status.ts +209 -0
- package/src/commands/router-mesh-session-owner.ts +114 -0
- package/src/commands/router-refine.ts +1794 -0
- package/src/commands/router-worktree-cleanup.ts +870 -0
- package/src/commands/router.ts +567 -4149
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +517 -24
- package/src/config/config.ts +30 -0
- package/src/config/mesh-config.ts +300 -24
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/registry-resolver.ts +100 -0
- package/src/config/repo-settings.ts +111 -0
- package/src/config/state-store.ts +55 -0
- 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 +94 -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 +112 -16
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +822 -48
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +261 -4
- package/src/git/index.ts +17 -0
- package/src/index.ts +190 -13
- 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/debug-config.ts +25 -0
- package/src/logging/debug-trace.ts +7 -2
- 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 +510 -0
- package/src/mesh/coordinator-prompt.ts +627 -34
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +706 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-completion-synthesis.ts +416 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +284 -0
- package/src/mesh/mesh-event-classify.ts +78 -0
- package/src/mesh/mesh-event-forwarding.ts +2030 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +32 -0
- package/src/mesh/mesh-events-pending.ts +1548 -0
- package/src/mesh/mesh-events-stale.ts +452 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +43 -1035
- package/src/mesh/mesh-fast-forward.ts +856 -0
- package/src/mesh/mesh-host-ownership.ts +41 -3
- package/src/mesh/mesh-init.ts +350 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +897 -102
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-missions.ts +695 -0
- package/src/mesh/mesh-node-identity.ts +2065 -0
- package/src/mesh/mesh-queue-assignment.ts +2498 -0
- package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
- package/src/mesh/mesh-reconcile-config.ts +66 -0
- package/src/mesh/mesh-reconcile-identity.ts +103 -0
- package/src/mesh/mesh-reconcile-loop.ts +1696 -0
- package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1792 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-remote-event-pull.ts +279 -0
- package/src/mesh/mesh-review-inbox.ts +308 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +2292 -0
- package/src/mesh/mesh-scheduling-runtime.ts +198 -0
- package/src/mesh/mesh-task-inflight.ts +70 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1489 -145
- package/src/mesh/preview-freshness.ts +163 -0
- package/src/mesh/refine-config.ts +129 -12
- package/src/mesh/worktree-bootstrap-config.ts +599 -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 +100 -6
- package/src/providers/chat-message-normalization.ts +126 -4
- package/src/providers/cli-provider-effect-format.ts +53 -0
- package/src/providers/cli-provider-history-dedup.ts +75 -0
- package/src/providers/cli-provider-input-prompt.ts +133 -0
- package/src/providers/cli-provider-instance-types.ts +131 -0
- package/src/providers/cli-provider-instance.ts +2771 -499
- package/src/providers/cli-provider-status-helpers.ts +123 -0
- package/src/providers/cli-provider-transcript-merge.ts +114 -0
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +207 -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 +105 -0
- package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -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 +617 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +71 -0
- package/src/providers/provider-instance.ts +24 -1
- package/src/providers/provider-loader.ts +700 -55
- package/src/providers/provider-schema.ts +93 -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 +261 -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 +538 -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 +246 -0
- package/src/providers/spec/cli-adapter.ts +1064 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1528 -0
- package/src/providers/spec/fsm-evaluator.ts +290 -0
- package/src/providers/spec/fsm-loader.ts +128 -0
- package/src/providers/spec/fsm-types.ts +301 -0
- package/src/providers/spec/native-history-executor.ts +1241 -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 +284 -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 +861 -14
- package/src/runtime-defaults.ts +39 -0
- package/src/session-host/managed-host.ts +218 -0
- package/src/session-host/runtime-surface.ts +20 -80
- package/src/sessions/registry.ts +44 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +148 -55
- package/src/status/builders.ts +69 -6
- package/src/status/chat-tail-hot-sessions.ts +117 -2
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +100 -27
- 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 +26 -6
- 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,18 @@ 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
|
+
// ARCH-REFACTOR R1: the mesh taskId this queued message belongs to. Carried so that
|
|
121
|
+
// when the queue is flushed (once the prior turn settles) the new turn is bound to
|
|
122
|
+
// its OWN task, not whatever scalar the session happens to hold at flush time.
|
|
123
|
+
meshTaskId?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
|
|
114
127
|
if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
|
|
115
128
|
if (maxChars <= 0) return '';
|
|
@@ -119,27 +132,63 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
|
|
|
119
132
|
return current.slice(-keepFromCurrent) + chunk;
|
|
120
133
|
}
|
|
121
134
|
|
|
135
|
+
// Force-send (mesh coordinator dispatch / reconcile redelivery) writes raw
|
|
136
|
+
// keystrokes straight into the PTY, bypassing the normal echo-wait submit
|
|
137
|
+
// pipeline.
|
|
138
|
+
//
|
|
139
|
+
// History of this path's failure modes:
|
|
140
|
+
// 1. Originally it wrote `text + sendKey` in a single PTY write with zero
|
|
141
|
+
// settle time. Injected into an idle TUI input box that was still entering
|
|
142
|
+
// its input-accepting state, the trailing submit key could be swallowed —
|
|
143
|
+
// prompt typed but never submitted ("text injected, Enter not pressed").
|
|
144
|
+
// 2. The first fix split inject from submit: write(text) → echo-gated settle →
|
|
145
|
+
// write(sendKey) as a *separate* PTY write. That broke win32: ConPTY/TUI
|
|
146
|
+
// does not recognize a lone '\r' that arrives in its own PTY chunk
|
|
147
|
+
// 150ms+ after the text as a submit key, and the win32 echo gate never
|
|
148
|
+
// matched (ConPTY echo/parsing differences), so the cap was burned and the
|
|
149
|
+
// same detached lone CR was emitted — adding latency while the Enter still
|
|
150
|
+
// got swallowed.
|
|
151
|
+
//
|
|
152
|
+
// Current behavior: keep a fixed settle gap so the input handler is in its
|
|
153
|
+
// input-accepting state, then write `content + sendKey` as a SINGLE atomic PTY
|
|
154
|
+
// write — the same verified mechanism the normal `immediate` submit strategy
|
|
155
|
+
// uses (submitImmediatePrompt). With the Enter in the same write unit as the
|
|
156
|
+
// text, win32 ConPTY always sees it as a submit and the race that the split was
|
|
157
|
+
// trying to solve cannot happen (the Enter can never be separated from the
|
|
158
|
+
// text). FORCE_SUBMIT_SETTLE_MS is that minimum pre-submit gap.
|
|
159
|
+
const FORCE_SUBMIT_SETTLE_MS = 150;
|
|
160
|
+
|
|
122
161
|
// ─── Adapter ────────────────────────────────────────
|
|
123
162
|
|
|
124
163
|
export class ProviderCliAdapter implements CliAdapter {
|
|
125
|
-
|
|
164
|
+
cliType: string;
|
|
126
165
|
readonly cliName: string;
|
|
127
166
|
public workingDir: string;
|
|
128
167
|
|
|
129
168
|
private provider: CliProviderModule;
|
|
130
169
|
private ptyProcess: PtyRuntimeTransport | null = null;
|
|
131
170
|
private transportFactory: PtyTransportFactory;
|
|
132
|
-
private currentStatus: CliSessionStatus['status'] = 'starting';
|
|
133
171
|
private onStatusChange: (() => void) | null = null;
|
|
134
172
|
|
|
173
|
+
// ─── State machine engine ─────────────────────────
|
|
174
|
+
readonly engine: CliStateEngine;
|
|
175
|
+
|
|
135
176
|
private responseBuffer = '';
|
|
136
177
|
private recentOutputBuffer = '';
|
|
137
|
-
private
|
|
138
|
-
private
|
|
139
|
-
private parseErrorMessage: string | null = null;
|
|
178
|
+
private get parseErrorMessage(): string | null { return this.runner.parseErrorMessage; }
|
|
179
|
+
private providerSessionId: string | null = null;
|
|
140
180
|
private responseTimeout: NodeJS.Timeout | null = null;
|
|
141
|
-
private idleTimeout: NodeJS.Timeout | null = null;
|
|
142
181
|
private ready = false;
|
|
182
|
+
// WIN32-READY-HOLD: the ready barrier can release on screen/spec-FSM grace
|
|
183
|
+
// before win32 ConPTY's input layer is live. The first split write (text, then a
|
|
184
|
+
// separate trailing CR via waitForEchoAndSubmit) then has its submit CR swallowed,
|
|
185
|
+
// and every CR-only retry re-sends a bare CR the input layer keeps dropping — the
|
|
186
|
+
// first message is typed-but-never-submitted and lost. Routing only the FIRST turn
|
|
187
|
+
// through the atomic content+sendKey single write (submitImmediatePrompt) keeps the
|
|
188
|
+
// Enter in the same PTY write unit as the text, the invariant win32 ConPTY needs to
|
|
189
|
+
// recognize a submit, so the swallow is bypassed. Subsequent turns (input layer now
|
|
190
|
+
// proven live) keep the normal echo-gated path. Flips true on first committed turn.
|
|
191
|
+
private firstTurnSent = false;
|
|
143
192
|
private startupBuffer = '';
|
|
144
193
|
private startupParseGate = false;
|
|
145
194
|
private startupSettleTimer: NodeJS.Timeout | null = null;
|
|
@@ -159,44 +208,71 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
159
208
|
private lastScreenSnapshot = '';
|
|
160
209
|
private lastScreenText = '';
|
|
161
210
|
private lastScreenSnapshotReadAt = Number.NEGATIVE_INFINITY;
|
|
211
|
+
// (FALSEIDLE Path-C) Count of CONSECUTIVE getStatus polls that observed a
|
|
212
|
+
// gate-eligible static-idle screen (detect=idle, no modal, quiet, empty
|
|
213
|
+
// partial buffer). For a mesh/autonomous worker we require several such
|
|
214
|
+
// polls in a row before confirming static-idle (see getStatus), so a
|
|
215
|
+
// single momentarily-silent point-sample of a still-live turn cannot flip
|
|
216
|
+
// it. Reset to 0 the instant any poll is ineligible.
|
|
217
|
+
private staticIdlePollStreak = 0;
|
|
162
218
|
|
|
163
219
|
// Server log forwarding
|
|
164
220
|
private serverConn: any = null;
|
|
165
221
|
private logBuffer: { message: string; level: string }[] = [];
|
|
166
222
|
|
|
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;
|
|
223
|
+
private pendingOutboundQueue: PendingOutboundMessage[] = [];
|
|
224
|
+
private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
|
|
225
|
+
private pendingOutboundFlushInFlight = false;
|
|
226
|
+
// Submit retry timer — PTY-level, not state machine
|
|
183
227
|
private submitRetryTimer: NodeJS.Timeout | null = null;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
228
|
+
|
|
229
|
+
// PTY-WRITE-SERIALIZE: a single per-session tail promise that serializes every
|
|
230
|
+
// PTY write. Each writeToPty() call chains its actual write after the previous
|
|
231
|
+
// one and returns a promise that resolves only after ITS write completes, so
|
|
232
|
+
// two consecutive sends (e.g. mesh force-dispatch burst → forceSendMessage, or
|
|
233
|
+
// a retry-timer's bare CR interleaving a fresh body) can never write into the
|
|
234
|
+
// same input line: message A's (body + sendKey/CR) is fully written to the PTY
|
|
235
|
+
// before message B's body write starts. The chain is kept alive across errors
|
|
236
|
+
// (each link swallows/logs its own failure) so one failed write never wedges
|
|
237
|
+
// all later writes behind a permanently-rejected tail.
|
|
238
|
+
private ptyWriteChain: Promise<void> = Promise.resolve();
|
|
189
239
|
|
|
190
240
|
// Resize redraw suppression
|
|
191
241
|
private resizeSuppressUntil: number = 0;
|
|
192
242
|
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
// (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
|
|
244
|
+
// ChatSourceMachine holds the lock by state, not by a mutable field on
|
|
245
|
+
// the adapter. Removed entirely; no callers remain after the readChat
|
|
246
|
+
// ladder was replaced.
|
|
247
|
+
|
|
248
|
+
// ─── Script runner (parsing isolated here, adapter stays as transport) ───
|
|
249
|
+
private readonly runner: CliScriptRunner;
|
|
250
|
+
/** @deprecated use runner.cliScripts for direct script access */
|
|
251
|
+
get cliScripts(): CliScripts { return this.runner.cliScripts; }
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Recent script invocations (oldest → newest). Exposed via the
|
|
255
|
+
* `get_chat_debug_bundle` daemon command so anyone debugging a
|
|
256
|
+
* stuck-status regression can read what each detectStatus /
|
|
257
|
+
* parseSession call actually saw and returned, instead of having
|
|
258
|
+
* to instrument the daemon.
|
|
259
|
+
*/
|
|
260
|
+
getScriptInvocationTrace() {
|
|
261
|
+
return this.runner.getInvocationTrace();
|
|
262
|
+
}
|
|
195
263
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Returns the full raw PTY byte stream captured since adapter start.
|
|
266
|
+
* Used by the `record_provider_pty` IPC command to produce fixtures.
|
|
267
|
+
* Bounded by MAX_ACCUMULATED_BUFFER; older bytes may have been dropped.
|
|
268
|
+
*/
|
|
269
|
+
getAccumulatedRawBuffer(): { text: string; droppedChars: number } {
|
|
270
|
+
return {
|
|
271
|
+
text: this.accumulatedRawBuffer,
|
|
272
|
+
droppedChars: this.accumulatedRawBufferDroppedChars,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
|
|
200
276
|
private runtimeSettings: Record<string, any> = {};
|
|
201
277
|
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
202
278
|
private accumulatedBuffer: string = '';
|
|
@@ -205,7 +281,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
205
281
|
/** Full accumulated raw PTY output (with ANSI) */
|
|
206
282
|
private accumulatedRawBuffer: string = '';
|
|
207
283
|
/** Current visible terminal screen snapshot */
|
|
208
|
-
private terminalScreen = new TerminalScreen(
|
|
284
|
+
private terminalScreen = new TerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
|
|
209
285
|
private static readonly MAX_RESPONSE_BUFFER = 8000;
|
|
210
286
|
private static readonly MAX_RECENT_OUTPUT_BUFFER = 1000;
|
|
211
287
|
private responseBufferDroppedChars = 0;
|
|
@@ -216,10 +292,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
216
292
|
* Hermes turn (tool calls + reasoning + final bubble) without the
|
|
217
293
|
* rolling window pushing the turn's ╭─ opening line out of view. */
|
|
218
294
|
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
295
|
private parsedStatusCache: {
|
|
224
296
|
responseBuffer: string;
|
|
225
297
|
currentTurnScope: TurnParseScope | null;
|
|
@@ -233,11 +305,15 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
233
305
|
result: any;
|
|
234
306
|
} | null = null;
|
|
235
307
|
private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
|
|
236
|
-
|
|
308
|
+
// (FALSEIDLE Path-C) Consecutive gate-eligible getStatus polls a mesh/autonomous
|
|
309
|
+
// session must show before the poll-static-idle confirm fires. 2 = one extra
|
|
310
|
+
// status tick of hysteresis: enough to reject a single momentary-silence
|
|
311
|
+
// point-sample of a still-live turn, cheap enough not to materially delay a
|
|
312
|
+
// genuine boot-wedge release (the wedge screen is stably static, so it clears
|
|
313
|
+
// every consecutive poll and confirms on the 2nd).
|
|
314
|
+
private static readonly STATIC_IDLE_POLL_CONFIRM_COUNT = 2;
|
|
237
315
|
|
|
238
316
|
private readonly providerResolutionMeta: ProviderResolutionMeta;
|
|
239
|
-
private static readonly FINISH_RETRY_DELAY_MS = 300;
|
|
240
|
-
private static readonly MAX_FINISH_RETRIES = 2;
|
|
241
317
|
|
|
242
318
|
private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
|
|
243
319
|
const build = (droppedChars: number, maxChars: number) => droppedChars > 0
|
|
@@ -313,13 +389,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
313
389
|
if (
|
|
314
390
|
cached
|
|
315
391
|
&& cached.responseBuffer === this.responseBuffer
|
|
316
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
392
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
317
393
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
318
394
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
319
395
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
320
396
|
&& cached.screenText === this.lastScreenText
|
|
321
|
-
&& cached.currentStatus === this.currentStatus
|
|
322
|
-
&& cached.activeModal === this.activeModal
|
|
397
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
398
|
+
&& cached.activeModal === this.engine.activeModal
|
|
323
399
|
&& cached.cliName === this.cliName
|
|
324
400
|
) {
|
|
325
401
|
return cached.result;
|
|
@@ -343,84 +419,18 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
343
419
|
return this.timeouts.statusActivityHold;
|
|
344
420
|
}
|
|
345
421
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
});
|
|
422
|
+
// (FALSEIDLE Path-C) Whether this session is a mesh worker or coordinator's
|
|
423
|
+
// own autonomous session. Mirrors CliProviderInstance.isAutonomousMeshSession
|
|
424
|
+
// over the runtimeSettings the instance mirrors down via updateRuntimeSettings
|
|
425
|
+
// (meshNodeFor / meshActiveTaskId / meshNodeId / launchedByCoordinator =
|
|
426
|
+
// isMeshWorkerSession, plus meshCoordinatorFor for the coordinator's own turn).
|
|
427
|
+
// Such a session has no human at the keyboard to correct a premature idle, so
|
|
428
|
+
// the poll-static-idle confirm is debounced for it (multiple consecutive idle
|
|
429
|
+
// polls) rather than fired on a single point-sample.
|
|
430
|
+
private isAutonomousMeshSession(): boolean {
|
|
431
|
+
const s = this.runtimeSettings;
|
|
432
|
+
return !!(s?.meshNodeFor || s?.meshActiveTaskId || s?.meshNodeId
|
|
433
|
+
|| s?.launchedByCoordinator || s?.meshCoordinatorFor);
|
|
424
434
|
}
|
|
425
435
|
|
|
426
436
|
// Resolved timeouts
|
|
@@ -432,7 +442,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
432
442
|
private readonly sendKey: string;
|
|
433
443
|
private readonly submitStrategy: 'wait_for_echo' | 'immediate';
|
|
434
444
|
private readonly requirePromptEchoBeforeSubmit: boolean;
|
|
435
|
-
private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
|
|
436
445
|
|
|
437
446
|
constructor(
|
|
438
447
|
provider: CliProviderModule,
|
|
@@ -441,6 +450,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
441
450
|
private extraEnv: Record<string, string> = {},
|
|
442
451
|
transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
|
|
443
452
|
) {
|
|
453
|
+
this.runner = new CliScriptRunner(provider.type);
|
|
444
454
|
this.provider = provider;
|
|
445
455
|
this.transportFactory = transportFactory;
|
|
446
456
|
this.cliType = provider.type;
|
|
@@ -458,10 +468,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
458
468
|
this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
|
|
459
469
|
this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
|
|
460
470
|
|
|
461
|
-
//
|
|
462
|
-
this.
|
|
463
|
-
|
|
464
|
-
|
|
471
|
+
// State machine engine — owns all status transitions
|
|
472
|
+
this.engine = new CliStateEngine(
|
|
473
|
+
provider,
|
|
474
|
+
this.runner,
|
|
475
|
+
this as unknown as CliTransportAccess,
|
|
476
|
+
{
|
|
477
|
+
onStatusChange: () => { this.onStatusChange?.(); },
|
|
478
|
+
onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
|
|
479
|
+
onTurnCompleted: () => { this.responseBuffer = ''; },
|
|
480
|
+
} satisfies CliStateEngineCallbacks,
|
|
481
|
+
resolvedConfig.timeouts,
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Scripts delegated to CliScriptRunner — adapter stays as transport.
|
|
485
|
+
// Pass the manifest tui block so the runner can build the
|
|
486
|
+
// declarativeDetectStatus / declarativeParseApproval SDK functions
|
|
487
|
+
// that v1 extended-tier overrides depend on (without these, scripts
|
|
488
|
+
// like codex-cli's detect_status v1 fail-closed with return 'idle'
|
|
489
|
+
// and the session sticks in `generating` forever).
|
|
490
|
+
this.runner.setScripts(provider.scripts || {}, provider.tui);
|
|
491
|
+
// Per-invocation wall-clock budget for provider scripts. Manifests may
|
|
492
|
+
// raise this for genuinely slow parsers, but the default (50ms) is the
|
|
493
|
+
// settle-loop safety net — any script that exceeds it gets flagged in
|
|
494
|
+
// the invocation trace and surfaced via the debug bundle so we can
|
|
495
|
+
// identify the offender instead of guessing why the settle loop hangs.
|
|
496
|
+
this.runner.setScriptCallBudget(provider.scriptCallBudgetMs ?? 50);
|
|
497
|
+
const scriptNames = this.runner.getScriptNames();
|
|
465
498
|
if (scriptNames.length > 0) {
|
|
466
499
|
LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
|
|
467
500
|
LOG.info(
|
|
@@ -487,14 +520,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
487
520
|
|
|
488
521
|
/** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
|
|
489
522
|
setCliScripts(scripts: CliScripts): void {
|
|
490
|
-
this.
|
|
523
|
+
this.runner.setScripts(scripts);
|
|
491
524
|
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(', ')}]`);
|
|
525
|
+
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
|
|
498
526
|
}
|
|
499
527
|
|
|
500
528
|
/** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
|
|
@@ -548,15 +576,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
548
576
|
});
|
|
549
577
|
|
|
550
578
|
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
579
|
|
|
561
580
|
try {
|
|
562
581
|
this.ptyProcess = this.transportFactory.spawn(
|
|
@@ -620,13 +639,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
620
639
|
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
621
640
|
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
|
|
622
641
|
this.flushPendingOutputParse();
|
|
623
|
-
this.recordTrace('exit', { exitCode });
|
|
624
642
|
this.ptyProcess = null;
|
|
625
|
-
this.
|
|
643
|
+
this.engine.onPtyExit();
|
|
626
644
|
this.ready = false;
|
|
627
645
|
this.startupParseGate = false;
|
|
628
646
|
this.spawnAt = 0;
|
|
629
|
-
this.
|
|
647
|
+
this.runner.resetSessionState();
|
|
630
648
|
this.onStatusChange?.();
|
|
631
649
|
});
|
|
632
650
|
|
|
@@ -635,17 +653,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
635
653
|
this.startupBuffer = '';
|
|
636
654
|
this.startupFirstOutputAt = 0;
|
|
637
655
|
if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
|
|
638
|
-
this.resetTerminalScreen(
|
|
656
|
+
this.resetTerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
|
|
639
657
|
this.pendingTerminalQueryTail = '';
|
|
640
|
-
this.currentTurnScope = null;
|
|
641
|
-
this.finishRetryCount = 0;
|
|
642
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
643
658
|
this.ready = false;
|
|
659
|
+
// Each fresh spawn re-enters the premature-ready swallow window — the next
|
|
660
|
+
// turn is again a "first turn" and must use the win32-safe atomic send path.
|
|
661
|
+
this.firstTurnSent = false;
|
|
644
662
|
await this.ptyProcess.ready;
|
|
645
|
-
this.
|
|
646
|
-
runtimeMeta: this.getRuntimeMetadata(),
|
|
647
|
-
});
|
|
648
|
-
this.setStatus('starting', 'pty_ready');
|
|
663
|
+
this.engine.onSpawnReady();
|
|
649
664
|
this.scheduleStartupSettleCheck();
|
|
650
665
|
this.onStatusChange?.();
|
|
651
666
|
}
|
|
@@ -671,11 +686,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
671
686
|
if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
|
|
672
687
|
this.startupFirstOutputAt = now;
|
|
673
688
|
}
|
|
674
|
-
if (
|
|
675
|
-
this.clearIdleFinishCandidate('new_output');
|
|
689
|
+
if (rawData.length > 0 || cleanData.length > 0) {
|
|
690
|
+
this.engine.clearIdleFinishCandidate('new_output');
|
|
676
691
|
}
|
|
677
692
|
if (getDebugRuntimeConfig().collectDebugTrace) {
|
|
678
|
-
this.
|
|
693
|
+
this.engine.recordExternalTrace('output', {
|
|
679
694
|
rawLength: rawData.length,
|
|
680
695
|
cleanLength: cleanData.length,
|
|
681
696
|
rawPreview: summarizeCliTraceText(rawData, 300),
|
|
@@ -687,7 +702,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
687
702
|
this.scheduleStartupSettleCheck();
|
|
688
703
|
}
|
|
689
704
|
|
|
690
|
-
if (this.isWaitingForResponse && cleanData) {
|
|
705
|
+
if (this.engine.isWaitingForResponse && cleanData) {
|
|
691
706
|
const previousResponseLen = this.responseBuffer.length;
|
|
692
707
|
this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
|
|
693
708
|
this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
|
|
@@ -726,19 +741,19 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
726
741
|
// Keep turn-scope offsets aligned with the truncated buffer so scoped
|
|
727
742
|
// parses don't lose the beginning of a long turn (e.g. the Hermes
|
|
728
743
|
// ╭─ opening line) when the rolling window sheds bytes.
|
|
729
|
-
if (this.currentTurnScope) {
|
|
744
|
+
if (this.engine.currentTurnScope) {
|
|
730
745
|
if (droppedClean > 0) {
|
|
731
|
-
this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
|
|
746
|
+
this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
|
|
732
747
|
}
|
|
733
748
|
if (droppedRaw > 0) {
|
|
734
|
-
this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
|
|
749
|
+
this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
|
|
735
750
|
}
|
|
736
751
|
}
|
|
737
752
|
|
|
738
753
|
this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
|
|
739
754
|
|
|
740
755
|
// ─── Script-based status detection
|
|
741
|
-
this.scheduleSettle();
|
|
756
|
+
this.engine.scheduleSettle();
|
|
742
757
|
}
|
|
743
758
|
|
|
744
759
|
private resolveStartupState(
|
|
@@ -761,6 +776,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
761
776
|
if (stableMs < 2000) return;
|
|
762
777
|
|
|
763
778
|
const startupModal = this.runParseApproval(this.recentOutputBuffer);
|
|
779
|
+
const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
780
|
+
if (!startupModal && startupStatus !== 'idle') {
|
|
781
|
+
this.scheduleStartupSettleCheck();
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
764
784
|
this.startupParseGate = false;
|
|
765
785
|
if (this.startupSettleTimer) {
|
|
766
786
|
clearTimeout(this.startupSettleTimer);
|
|
@@ -768,20 +788,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
768
788
|
}
|
|
769
789
|
this.ready = true;
|
|
770
790
|
if (startupModal) {
|
|
771
|
-
this.activeModal = startupModal;
|
|
772
|
-
this.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
791
|
+
this.engine.activeModal = startupModal;
|
|
792
|
+
this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
|
|
773
793
|
} else {
|
|
774
|
-
if (this.currentStatus === 'waiting_approval' || this.activeModal) {
|
|
775
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
794
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
|
|
795
|
+
this.engine.lastApprovalResolvedAt = Date.now();
|
|
776
796
|
}
|
|
777
|
-
this.activeModal = null;
|
|
778
|
-
|
|
797
|
+
this.engine.activeModal = null;
|
|
798
|
+
// Clear the in-flight turn flag at the same time we declare
|
|
799
|
+
// startup-idle. Otherwise the next settled evaluation sees
|
|
800
|
+
// isWaitingForResponse=true + recent CLI welcome-screen paints
|
|
801
|
+
// and flips us right back to generating via the hold path
|
|
802
|
+
// (the "startup → generating → idle → generating → idle"
|
|
803
|
+
// flicker the user observed on claude-cli launch).
|
|
804
|
+
this.engine.isWaitingForResponse = false;
|
|
805
|
+
this.engine.currentTurnScope = null;
|
|
806
|
+
this.engine.setStatus('idle', `startup_ready:${trigger}`);
|
|
779
807
|
}
|
|
780
808
|
LOG.info(
|
|
781
809
|
'CLI',
|
|
782
810
|
`[${this.cliType}] Startup settled (${trigger}, stableMs=${stableMs}, modal=${!!startupModal}) providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
|
|
783
811
|
);
|
|
784
812
|
this.onStatusChange?.();
|
|
813
|
+
// Readiness barrier flush: a message queued because the session was not yet
|
|
814
|
+
// ready (sendMessageNow's not_ready_pending_prompt path) has no turn-completion
|
|
815
|
+
// event to trigger its flush. Now that the interactive prompt is up and we are
|
|
816
|
+
// idle, drain it. No-op when the queue is empty or we settled to a modal.
|
|
817
|
+
if (!startupModal) this.schedulePendingOutboundFlush();
|
|
785
818
|
}
|
|
786
819
|
|
|
787
820
|
private scheduleStartupSettleCheck(): void {
|
|
@@ -801,88 +834,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
801
834
|
}, delayMs);
|
|
802
835
|
}
|
|
803
836
|
|
|
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
837
|
private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
|
|
887
838
|
const startedAt = Date.now();
|
|
888
839
|
let loggedWait = false;
|
|
@@ -892,7 +843,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
892
843
|
const screenText = this.terminalScreen.getText() || '';
|
|
893
844
|
const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
|
|
894
845
|
const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
895
|
-
const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
|
|
846
|
+
const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
|
|
896
847
|
const interactiveReady = status === 'idle'
|
|
897
848
|
&& stableMs >= 700
|
|
898
849
|
&& recentlyOutput >= 350;
|
|
@@ -926,637 +877,84 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
926
877
|
|
|
927
878
|
private clearAllTimers(): void {
|
|
928
879
|
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
880
|
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
881
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
936
882
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
883
|
+
this.engine.clearAllTimers();
|
|
937
884
|
}
|
|
938
885
|
|
|
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
|
-
}
|
|
886
|
+
// ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
|
|
958
887
|
|
|
959
|
-
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
978
|
-
.trim();
|
|
979
|
-
return remainder.length > 0;
|
|
980
|
-
}
|
|
981
|
-
return true;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
private evaluateSettled(): void {
|
|
985
|
-
const now = Date.now();
|
|
986
|
-
if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
|
|
987
|
-
const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
|
|
988
|
-
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
989
|
-
this.settleTimer = setTimeout(() => {
|
|
990
|
-
this.settleTimer = null;
|
|
991
|
-
this.settledBuffer = this.recentOutputBuffer;
|
|
992
|
-
this.evaluateSettled();
|
|
993
|
-
}, delayTime);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
this.resolveStartupState('settled');
|
|
998
|
-
if (this.startupParseGate) return;
|
|
999
|
-
|
|
1000
|
-
if (!this.isWaitingForResponse && !this.currentTurnScope && !this.activeModal && !this.parseErrorMessage) {
|
|
1001
|
-
const tail = this.settledBuffer || this.recentOutputBuffer;
|
|
1002
|
-
const modal = this.runParseApproval(tail);
|
|
1003
|
-
const lightweightStatus = this.cliScripts?.detectStatus
|
|
1004
|
-
? this.runDetectStatus(tail)
|
|
1005
|
-
: null;
|
|
1006
|
-
if (!modal && lightweightStatus === 'idle' && this.currentStatus === 'idle') {
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
const session = this.runParseSession();
|
|
1012
|
-
if (!session) return;
|
|
1013
|
-
|
|
1014
|
-
const { status, messages, parsedStatus } = session;
|
|
1015
|
-
const modal = (session as any).activeModal ?? session.modal ?? null;
|
|
1016
|
-
const parsedMessages = normalizeCliParsedMessages(messages, {
|
|
1017
|
-
scope: null,
|
|
1018
|
-
lastOutputAt: this.lastOutputAt,
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages)) return;
|
|
1022
|
-
|
|
1023
|
-
const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
|
|
1024
|
-
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || '');
|
|
1025
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1026
|
-
|
|
1027
|
-
this.recordTrace('settled', {
|
|
1028
|
-
tail: summarizeCliTraceText(this.settledBuffer, 500),
|
|
1029
|
-
screenText: summarizeCliTraceText(screenText, 1200),
|
|
1030
|
-
detectStatus: status,
|
|
1031
|
-
parsedStatus: parsedStatus || null,
|
|
1032
|
-
parsedMessageCount: parsedMessages.length,
|
|
1033
|
-
parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
|
|
1034
|
-
parsedActiveModal: modal,
|
|
1035
|
-
approval: modal,
|
|
1036
|
-
...buildCliTraceParseSnapshot({
|
|
1037
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1038
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1039
|
-
responseBuffer: this.responseBuffer,
|
|
1040
|
-
partialResponse: this.responseBuffer,
|
|
1041
|
-
scope: this.currentTurnScope,
|
|
1042
|
-
}),
|
|
888
|
+
runParseSession(): ParsedSession | null {
|
|
889
|
+
const screenText = this.terminalScreen.getText();
|
|
890
|
+
const parseScreenText = this.getParseScreenText(screenText);
|
|
891
|
+
const tail = this.recentOutputBuffer.slice(-500);
|
|
892
|
+
const input = buildCliParseInput({
|
|
893
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
894
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
895
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
896
|
+
terminalScreenText: parseScreenText,
|
|
897
|
+
workingDir: this.workingDir,
|
|
898
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
899
|
+
historySessionId: this.providerSessionId || undefined,
|
|
900
|
+
baseMessages: [],
|
|
901
|
+
partialResponse: this.responseBuffer,
|
|
902
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
903
|
+
scope: this.engine.currentTurnScope,
|
|
904
|
+
runtimeSettings: this.runtimeSettings,
|
|
905
|
+
spawnAt: this.spawnAt,
|
|
1043
906
|
});
|
|
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
|
-
}),
|
|
907
|
+
const session = this.runner.parseSession({
|
|
908
|
+
...input,
|
|
909
|
+
tail,
|
|
910
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1168
911
|
});
|
|
1169
|
-
this.
|
|
912
|
+
if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
|
|
913
|
+
return session;
|
|
1170
914
|
}
|
|
1171
915
|
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
this.
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
this.idleTimeout = setTimeout(() => {
|
|
1183
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1184
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1185
|
-
this.finishResponse();
|
|
1186
|
-
}
|
|
1187
|
-
}, this.timeouts.generatingIdle);
|
|
1188
|
-
} else {
|
|
1189
|
-
this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
|
|
1190
|
-
}
|
|
1191
|
-
this.onStatusChange?.();
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
if (!inCooldown) {
|
|
1195
|
-
if (!modal) {
|
|
1196
|
-
LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
this.isWaitingForResponse = true;
|
|
1200
|
-
this.setStatus('waiting_approval', 'script_detect');
|
|
1201
|
-
this.activeModal = modal;
|
|
1202
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1203
|
-
this.armApprovalExitTimeout();
|
|
1204
|
-
this.onStatusChange?.();
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
private applyGenerating(ctx: SettledEvalContext): void {
|
|
1209
|
-
const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
|
|
1210
|
-
this.clearIdleFinishCandidate('generating');
|
|
1211
|
-
const screenText = this.terminalScreen.getText() || '';
|
|
1212
|
-
const effectiveScreenText = screenText || this.accumulatedBuffer;
|
|
1213
|
-
const noActiveTurn = !this.currentTurnScope;
|
|
1214
|
-
const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
|
|
1215
|
-
const parsedShowsLiveAssistantProgress = parsedStatus === 'generating'
|
|
1216
|
-
&& !!lastParsedAssistant
|
|
1217
|
-
;
|
|
1218
|
-
if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (prevStatus === 'waiting_approval') {
|
|
1222
|
-
// Transitioned out of approval → generating
|
|
1223
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1224
|
-
this.activeModal = null;
|
|
1225
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1226
|
-
}
|
|
1227
|
-
if (!this.isWaitingForResponse) {
|
|
1228
|
-
this.isWaitingForResponse = true;
|
|
1229
|
-
this.responseBuffer = '';
|
|
1230
|
-
}
|
|
1231
|
-
this.setStatus('generating', 'script_detect');
|
|
1232
|
-
// Reset idle timeout
|
|
1233
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1234
|
-
this.idleTimeout = setTimeout(() => {
|
|
1235
|
-
if (this.isWaitingForResponse) {
|
|
1236
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1237
|
-
this.finishResponse();
|
|
1238
|
-
}
|
|
1239
|
-
}, this.timeouts.generatingIdle);
|
|
1240
|
-
this.onStatusChange?.();
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
private applyIdle(ctx: SettledEvalContext, now: number): void {
|
|
1244
|
-
const { modal, lastParsedAssistant, prevStatus } = ctx;
|
|
1245
|
-
if (prevStatus === 'waiting_approval') {
|
|
1246
|
-
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1247
|
-
this.activeModal = null;
|
|
1248
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
1249
|
-
this.setStatus('idle', 'approval_prompt_gone_script_idle');
|
|
1250
|
-
}
|
|
1251
|
-
if (!this.isWaitingForResponse) {
|
|
1252
|
-
if (prevStatus !== 'idle') {
|
|
1253
|
-
this.clearIdleFinishCandidate('idle_without_response');
|
|
1254
|
-
this.setStatus('idle', 'script_detect');
|
|
1255
|
-
this.onStatusChange?.();
|
|
1256
|
-
}
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
1260
|
-
const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
|
|
1261
|
-
const hasAssistantTurn = !!lastParsedAssistant;
|
|
1262
|
-
const assistantLength = lastParsedAssistant?.content?.length || 0;
|
|
1263
|
-
const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
|
|
1264
|
-
const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
|
|
1265
|
-
const idleReady = !modal
|
|
1266
|
-
&& hasAssistantTurn
|
|
1267
|
-
&& quietForMs >= idleQuietThresholdMs
|
|
1268
|
-
&& screenStableMs >= idleFinishConfirmMs;
|
|
1269
|
-
const candidate = this.idleFinishCandidate;
|
|
1270
|
-
const candidateQuiet = !!candidate
|
|
1271
|
-
&& candidate.responseEpoch === this.responseEpoch
|
|
1272
|
-
&& candidate.lastOutputAt === this.lastOutputAt
|
|
1273
|
-
&& candidate.lastScreenChangeAt === this.lastScreenChangeAt
|
|
1274
|
-
&& assistantLength >= candidate.assistantLength
|
|
1275
|
-
&& (now - candidate.armedAt) >= idleFinishConfirmMs;
|
|
1276
|
-
|
|
1277
|
-
this.recordTrace('idle_decision', {
|
|
1278
|
-
quietForMs,
|
|
1279
|
-
screenStableMs,
|
|
1280
|
-
hasAssistantTurn,
|
|
1281
|
-
assistantLength,
|
|
1282
|
-
hasModal: !!modal,
|
|
1283
|
-
idleQuietThresholdMs,
|
|
1284
|
-
idleStableThresholdMs: idleFinishConfirmMs,
|
|
1285
|
-
idleReady,
|
|
1286
|
-
idleFinishConfirmMs,
|
|
1287
|
-
idleFinishCandidate: candidate,
|
|
1288
|
-
candidateQuiet,
|
|
1289
|
-
canFinishImmediately: idleReady && candidateQuiet,
|
|
1290
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
1291
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
1292
|
-
...buildCliTraceParseSnapshot({
|
|
1293
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1294
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1295
|
-
responseBuffer: this.responseBuffer,
|
|
1296
|
-
partialResponse: this.responseBuffer,
|
|
1297
|
-
scope: this.currentTurnScope,
|
|
1298
|
-
}),
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
if (idleReady && candidateQuiet) {
|
|
1302
|
-
this.clearIdleFinishCandidate('finish_response');
|
|
1303
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1304
|
-
this.finishResponse();
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (idleReady) {
|
|
1309
|
-
if (!candidate) {
|
|
1310
|
-
this.armIdleFinishCandidate(assistantLength);
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
} else {
|
|
1314
|
-
this.clearIdleFinishCandidate('idle_not_ready');
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1318
|
-
this.idleTimeout = setTimeout(() => {
|
|
1319
|
-
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1320
|
-
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
1321
|
-
this.clearIdleFinishCandidate('idle_timeout_finish');
|
|
1322
|
-
this.finishResponse();
|
|
1323
|
-
}
|
|
1324
|
-
}, this.timeouts.idleFinish);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
private finishResponse(): void {
|
|
1328
|
-
if (this.submitPendingUntil > Date.now()) return;
|
|
1329
|
-
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
1330
|
-
this.clearIdleFinishCandidate('finish_response_enter');
|
|
1331
|
-
this.recordTrace('finish_response', {
|
|
1332
|
-
...buildCliTraceParseSnapshot({
|
|
1333
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1334
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1335
|
-
responseBuffer: this.responseBuffer,
|
|
1336
|
-
partialResponse: this.responseBuffer,
|
|
1337
|
-
scope: this.currentTurnScope,
|
|
1338
|
-
}),
|
|
916
|
+
runDetectStatus(text: string): string | null {
|
|
917
|
+
const screenText = this.terminalScreen.getText();
|
|
918
|
+
const tail = text.slice(-500);
|
|
919
|
+
return this.runner.detectStatus({
|
|
920
|
+
tail,
|
|
921
|
+
screenText,
|
|
922
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
923
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
924
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
925
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1339
926
|
});
|
|
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
927
|
}
|
|
1376
928
|
|
|
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),
|
|
929
|
+
runParseApproval(tail: string): { message: string; buttons: string[] } | null {
|
|
930
|
+
const screenText = this.terminalScreen.getText();
|
|
931
|
+
const buffer = screenText || this.accumulatedBuffer;
|
|
932
|
+
return this.runner.parseApproval({
|
|
933
|
+
buffer,
|
|
934
|
+
screenText,
|
|
935
|
+
rawBuffer: this.accumulatedRawBuffer,
|
|
936
|
+
tail,
|
|
937
|
+
screen: buildCliScreenSnapshot(screenText),
|
|
938
|
+
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
939
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
1408
940
|
});
|
|
1409
|
-
return true;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
private commitCurrentTranscript(): { hasAssistant: boolean; assistantContent: string } {
|
|
1413
|
-
const parsed = this.parseCurrentTranscript(
|
|
1414
|
-
[],
|
|
1415
|
-
this.responseBuffer,
|
|
1416
|
-
this.currentTurnScope,
|
|
1417
|
-
);
|
|
1418
|
-
if (parsed && Array.isArray(parsed.messages)) {
|
|
1419
|
-
const parsedMessages = normalizeCliParsedMessages(parsed.messages, {
|
|
1420
|
-
scope: null,
|
|
1421
|
-
lastOutputAt: this.lastOutputAt,
|
|
1422
|
-
});
|
|
1423
|
-
const lastAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
|
|
1424
|
-
if (this.currentTurnScope) {
|
|
1425
|
-
LOG.info(
|
|
1426
|
-
'CLI',
|
|
1427
|
-
`[${this.cliType}] commitCurrentTranscript parserMessages=${parsedMessages.length} finalLastAssistant=${JSON.stringify(summarizeCliTraceText(lastAssistant?.content || '', 220)).slice(0, 260)}`
|
|
1428
|
-
);
|
|
1429
|
-
}
|
|
1430
|
-
this.recordTrace('commit_transcript', {
|
|
1431
|
-
parsedStatus: parsed.status || null,
|
|
1432
|
-
messageCount: parsedMessages.length,
|
|
1433
|
-
lastAssistant: lastAssistant ? summarizeCliTraceText(lastAssistant.content, 320) : '',
|
|
1434
|
-
messages: summarizeCliTraceMessages(parsedMessages),
|
|
1435
|
-
...buildCliTraceParseSnapshot({
|
|
1436
|
-
accumulatedBuffer: this.accumulatedBuffer,
|
|
1437
|
-
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1438
|
-
responseBuffer: this.responseBuffer,
|
|
1439
|
-
partialResponse: this.responseBuffer,
|
|
1440
|
-
scope: this.currentTurnScope,
|
|
1441
|
-
}),
|
|
1442
|
-
});
|
|
1443
|
-
if (!lastAssistant && this.currentTurnScope) {
|
|
1444
|
-
LOG.warn(
|
|
1445
|
-
'CLI',
|
|
1446
|
-
`[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
const hasAssistant = !!lastAssistant;
|
|
1450
|
-
return {
|
|
1451
|
-
hasAssistant,
|
|
1452
|
-
assistantContent: lastAssistant?.content || '',
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
|
-
if (this.currentTurnScope) {
|
|
1456
|
-
LOG.info(
|
|
1457
|
-
'CLI',
|
|
1458
|
-
`[${this.cliType}] commitCurrentTranscript parsed.messages=none responseBufferLen=${this.responseBuffer.length} accumulatedBufferLen=${this.accumulatedBuffer.length} parsedStatus=${parsed?.status || '-'} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
|
|
1459
|
-
);
|
|
1460
|
-
}
|
|
1461
|
-
return {
|
|
1462
|
-
hasAssistant: false,
|
|
1463
|
-
assistantContent: '',
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
// ─── Script Execution ──────────────────────────
|
|
1469
|
-
|
|
1470
|
-
private invokeCliScript<T>(script: Function, input: any): T {
|
|
1471
|
-
const hasStateFactory = typeof this.cliScripts?.createState === 'function';
|
|
1472
|
-
const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
|
|
1473
|
-
return expectsStateArgument
|
|
1474
|
-
? script(this.scriptState, input)
|
|
1475
|
-
: script(input);
|
|
1476
941
|
}
|
|
1477
942
|
|
|
1478
|
-
private
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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;
|
|
943
|
+
private applyParsedSessionMetadata(parsed: any): void {
|
|
944
|
+
const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
|
|
945
|
+
? parsed.providerSessionId.trim()
|
|
946
|
+
: '';
|
|
947
|
+
if (providerSessionId && providerSessionId !== this.providerSessionId) {
|
|
948
|
+
this.providerSessionId = providerSessionId;
|
|
949
|
+
this.updateRuntimeMeta({ providerSessionId });
|
|
1548
950
|
}
|
|
1549
951
|
}
|
|
1550
952
|
|
|
1551
|
-
private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
|
|
1552
|
-
return !!(startupModal || this.activeModal);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
953
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1556
954
|
if (this.parseErrorMessage) return 'error';
|
|
1557
|
-
if (this.
|
|
1558
|
-
if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
|
|
1559
|
-
return this.currentStatus;
|
|
955
|
+
if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
|
|
956
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
|
|
957
|
+
return this.engine.currentStatus;
|
|
1560
958
|
}
|
|
1561
959
|
|
|
1562
960
|
// ─── Public API (CliAdapter) ───────────────────
|
|
@@ -1564,17 +962,154 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1564
962
|
getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
|
|
1565
963
|
const allowParse = options.allowParse !== false;
|
|
1566
964
|
const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
965
|
+
const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
|
|
966
|
+
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
967
|
+
: null;
|
|
968
|
+
// (D4) Poll-driven static-idle confirm. A hosted CLI session whose boot
|
|
969
|
+
// banner drove the FSM to 'generating' can then sit at a STATIC ready
|
|
970
|
+
// prompt emitting NO further PTY output — every output-driven busy→idle
|
|
971
|
+
// re-eval is starved and the startup-settle loop has hard-stopped past
|
|
972
|
+
// spawnAt+10s, so currentStatus stays frozen at generating and the
|
|
973
|
+
// dashboard disables Send. This read-path poll is the only place that can
|
|
974
|
+
// release the wedge. Gate PRECISELY (reuse resolveStartupState's proven
|
|
975
|
+
// predicates so a real generating turn is NEVER mis-flipped):
|
|
976
|
+
// (a) no recent output for >= statusActivityHold (the 2000ms stable
|
|
977
|
+
// window resolveStartupState uses),
|
|
978
|
+
// (b) runDetectStatus(current screen) === 'idle' (same detector),
|
|
979
|
+
// (c) no active/parsed modal — an approval/choice screen is never
|
|
980
|
+
// flipped to idle.
|
|
981
|
+
// A real generating turn keeps producing output (fresh
|
|
982
|
+
// lastNonEmptyOutputAt) and/or shows 'esc to cancel' (detects busy) → it
|
|
983
|
+
// fails (a) or (b). confirmPollStaticIdle adds the final structural guard
|
|
984
|
+
// (currentStatus==='generating' AND no currentTurnScope/activeModal), so
|
|
985
|
+
// this only releases the boot-banner wedge and the post-turn static-idle
|
|
986
|
+
// case. Direct-spawn sessions already settle idle in the startup window
|
|
987
|
+
// → this is a no-op for them.
|
|
988
|
+
if (
|
|
989
|
+
allowParse
|
|
990
|
+
&& this.engine.currentStatus === 'generating'
|
|
991
|
+
&& !this.engine.currentTurnScope
|
|
992
|
+
&& !this.engine.activeModal
|
|
993
|
+
) {
|
|
994
|
+
const now = Date.now();
|
|
995
|
+
const quietForMs = this.lastNonEmptyOutputAt
|
|
996
|
+
? (now - this.lastNonEmptyOutputAt)
|
|
997
|
+
: Number.MAX_SAFE_INTEGER;
|
|
998
|
+
let eligible = false;
|
|
999
|
+
if (quietForMs >= this.getStatusActivityHoldMs()) {
|
|
1000
|
+
const screenText = this.terminalScreen.getText();
|
|
1001
|
+
const pollDetect = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
1002
|
+
const pollModal = this.runParseApproval(screenText)
|
|
1003
|
+
|| this.runParseApproval(this.recentOutputBuffer);
|
|
1004
|
+
// (FALSEIDLE Path-C) Final-assistant / pending-response discriminator.
|
|
1005
|
+
// Paths A and B refuse to finalize a turn whose partial-response buffer
|
|
1006
|
+
// is still non-empty (getCompletedFinalizationBlock 'partial_response_pending'
|
|
1007
|
+
// / completionFinalAssistantEvidence turnClosed at cli-provider-instance.ts).
|
|
1008
|
+
// Path C (this poll) previously OMITTED it, so a genuinely-live but
|
|
1009
|
+
// momentarily-silent turn — silent thinking, a backgrounded/long tool child,
|
|
1010
|
+
// the gap between two assistant bubbles — whose currentTurnScope anchor was
|
|
1011
|
+
// lost still satisfied the weaker gate and flipped to idle prematurely.
|
|
1012
|
+
// Require an EMPTY partial buffer here too. getPartialResponse() returns the
|
|
1013
|
+
// accumulated assistant stream while isWaitingForResponse (which
|
|
1014
|
+
// applyGenerating leaves set on the boot-banner wedge too), so this does NOT
|
|
1015
|
+
// reintroduce the D4b wedge: the attach/boot-banner seeds only the static
|
|
1016
|
+
// ready screen — no assistant turn ever streamed — so its partial buffer is
|
|
1017
|
+
// empty and the gate still releases it. A mid-turn quiet gap holds a
|
|
1018
|
+
// non-empty buffer and is deferred.
|
|
1019
|
+
const partial = this.getPartialResponse();
|
|
1020
|
+
const partialPending = typeof partial === 'string' && partial.trim().length > 0;
|
|
1021
|
+
eligible = pollDetect === 'idle' && !pollModal && !partialPending;
|
|
1022
|
+
}
|
|
1023
|
+
if (eligible) {
|
|
1024
|
+
// (FALSEIDLE Path-C) Debounce for autonomous mesh sessions. A worker /
|
|
1025
|
+
// coordinator has no human to correct a premature idle, and a single
|
|
1026
|
+
// runDetectStatus point-sample can land in a live turn's momentary silence.
|
|
1027
|
+
// Require STATIC_IDLE_POLL_CONFIRM_COUNT consecutive eligible polls before
|
|
1028
|
+
// confirming, so the FSM must observe a sustained static-idle screen — a
|
|
1029
|
+
// turn that resumes (fresh output, or a re-armed turn scope) resets the
|
|
1030
|
+
// streak. The status poll runs on the 30s-idle / 5s-generating heartbeat and
|
|
1031
|
+
// this getStatus gate is re-hit each dashboard status tick, so 2 confirms is
|
|
1032
|
+
// ~one extra tick of hysteresis — enough to reject a one-sample silence gap
|
|
1033
|
+
// without materially delaying a genuine boot-wedge release. Foreground /
|
|
1034
|
+
// attended sessions keep the single-poll confirm (a human is watching Send).
|
|
1035
|
+
const requiredStreak = this.isAutonomousMeshSession()
|
|
1036
|
+
? ProviderCliAdapter.STATIC_IDLE_POLL_CONFIRM_COUNT
|
|
1037
|
+
: 1;
|
|
1038
|
+
this.staticIdlePollStreak += 1;
|
|
1039
|
+
if (this.staticIdlePollStreak >= requiredStreak) {
|
|
1040
|
+
this.engine.confirmPollStaticIdle('poll_static_idle');
|
|
1041
|
+
this.staticIdlePollStreak = 0;
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
this.staticIdlePollStreak = 0;
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
this.staticIdlePollStreak = 0;
|
|
1048
|
+
}
|
|
1567
1049
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1568
|
-
let effectiveModal = startupModal || this.activeModal;
|
|
1050
|
+
let effectiveModal = startupModal || this.engine.activeModal;
|
|
1051
|
+
// (fix) When we have no captured modal yet, take one more live attempt
|
|
1052
|
+
// with the current screen text — the engine's settle pass can miss
|
|
1053
|
+
// the modal when it happens to fire exactly between writes, and
|
|
1054
|
+
// without a modal here the dashboard could never show the buttons.
|
|
1055
|
+
// We deliberately do NOT overwrite an existing engine.activeModal so
|
|
1056
|
+
// a stable matched modal wins. This runs even outside the startup
|
|
1057
|
+
// gate because Claude's approval frames can appear long after launch.
|
|
1058
|
+
if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
|
|
1059
|
+
const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
|
|
1060
|
+
if (liveDetect === 'waiting_approval') {
|
|
1061
|
+
const liveModal = this.runParseApproval(this.terminalScreen.getText())
|
|
1062
|
+
|| this.runParseApproval(this.recentOutputBuffer);
|
|
1063
|
+
// (fix) Only surface modals that have at least one non-empty
|
|
1064
|
+
// button. Half-rendered approval frames produced
|
|
1065
|
+
// { message, buttons: [] } briefly — auto-approve would then
|
|
1066
|
+
// pick buttonIndex=-1 and resolveModal would write the
|
|
1067
|
+
// provider's index-0 key into the prompt. Skip those frames
|
|
1068
|
+
// and let a later evaluate find the complete modal.
|
|
1069
|
+
const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
|
|
1070
|
+
&& liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
|
|
1071
|
+
if (liveModal && buttonsOk) {
|
|
1072
|
+
effectiveModal = liveModal;
|
|
1073
|
+
if (!this.engine.activeModal) this.engine.activeModal = liveModal;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// Only surface waiting_approval when we ALSO have a concrete modal
|
|
1078
|
+
// (message + buttons). detectStatus alone can fire while parseApproval
|
|
1079
|
+
// is still null — the engine logs "detectStatus=waiting_approval but
|
|
1080
|
+
// parseApproval returned null; ignoring". Without this guard getStatus
|
|
1081
|
+
// was shipping a bare waiting_approval with activeModal=null and the
|
|
1082
|
+
// user perceived the flow as broken.
|
|
1083
|
+
if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
|
|
1084
|
+
effectiveStatus = 'waiting_approval';
|
|
1085
|
+
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
1086
|
+
effectiveStatus = 'idle';
|
|
1087
|
+
}
|
|
1569
1088
|
if (allowParse && !startupModal && !effectiveModal) {
|
|
1570
1089
|
const parsed = this.getFreshParsedStatusCache();
|
|
1571
1090
|
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
1572
1091
|
&& parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
|
|
1573
1092
|
? parsed.activeModal
|
|
1574
1093
|
: null;
|
|
1094
|
+
const hasFinalAssistant = (p: any) => {
|
|
1095
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1096
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1097
|
+
};
|
|
1575
1098
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1576
1099
|
effectiveStatus = 'waiting_approval';
|
|
1577
1100
|
effectiveModal = parsedModal;
|
|
1101
|
+
} else if (
|
|
1102
|
+
effectiveStatus === 'idle'
|
|
1103
|
+
&& parsed?.status === 'generating'
|
|
1104
|
+
&& !hasFinalAssistant(parsed)
|
|
1105
|
+
) {
|
|
1106
|
+
effectiveStatus = 'generating';
|
|
1107
|
+
} else if (
|
|
1108
|
+
effectiveStatus === 'generating'
|
|
1109
|
+
&& parsed?.status === 'idle'
|
|
1110
|
+
&& hasFinalAssistant(parsed)
|
|
1111
|
+
) {
|
|
1112
|
+
effectiveStatus = 'idle';
|
|
1578
1113
|
}
|
|
1579
1114
|
}
|
|
1580
1115
|
const bufferState = this.getBufferState();
|
|
@@ -1583,8 +1118,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1583
1118
|
messages: [],
|
|
1584
1119
|
workingDir: this.workingDir,
|
|
1585
1120
|
activeModal: effectiveModal,
|
|
1586
|
-
|
|
1587
|
-
|
|
1121
|
+
approvalEntrySeq: this.engine.approvalEntrySeq,
|
|
1122
|
+
lastResolvedEntrySeq: this.engine.lastResolvedEntrySeq,
|
|
1123
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
1124
|
+
pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
|
|
1125
|
+
id: message.id,
|
|
1126
|
+
role: message.role,
|
|
1127
|
+
content: message.content,
|
|
1128
|
+
queuedAt: message.queuedAt,
|
|
1129
|
+
source: message.source,
|
|
1130
|
+
})),
|
|
1131
|
+
errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
|
|
1132
|
+
errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
|
|
1133
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1134
|
+
lastOutputAt: this.lastOutputAt,
|
|
1135
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
1588
1136
|
...(bufferState ? { bufferState } : {}),
|
|
1589
1137
|
};
|
|
1590
1138
|
}
|
|
@@ -1600,15 +1148,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1600
1148
|
const cached = this.parsedStatusCache;
|
|
1601
1149
|
const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
|
|
1602
1150
|
if (
|
|
1603
|
-
|
|
1151
|
+
!this.providerOwnsTranscript()
|
|
1152
|
+
&& cached
|
|
1604
1153
|
&& cached.responseBuffer === this.responseBuffer
|
|
1605
|
-
&& cached.currentTurnScope === this.currentTurnScope
|
|
1154
|
+
&& cached.currentTurnScope === this.engine.currentTurnScope
|
|
1606
1155
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
1607
1156
|
&& cached.accumulatedBuffer === this.accumulatedBuffer
|
|
1608
1157
|
&& cached.accumulatedRawBufferKey === accumulatedRawBufferKey
|
|
1609
1158
|
&& cached.screenText === parseScreenText
|
|
1610
|
-
&& cached.currentStatus === this.currentStatus
|
|
1611
|
-
&& cached.activeModal === this.activeModal
|
|
1159
|
+
&& cached.currentStatus === this.engine.currentStatus
|
|
1160
|
+
&& cached.activeModal === this.engine.activeModal
|
|
1612
1161
|
&& cached.cliName === this.cliName
|
|
1613
1162
|
) {
|
|
1614
1163
|
return cached.result;
|
|
@@ -1623,14 +1172,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1623
1172
|
const bufferState = this.getBufferState();
|
|
1624
1173
|
const result = {
|
|
1625
1174
|
id: (parsed as any).id || 'cli_session',
|
|
1626
|
-
status: parsed.status || this.currentStatus,
|
|
1175
|
+
status: parsed.status || this.engine.currentStatus,
|
|
1627
1176
|
title: (parsed as any).title || this.cliName,
|
|
1628
1177
|
messages: normalizeCliParsedMessages(parsed.messages, {
|
|
1629
1178
|
scope: null,
|
|
1630
1179
|
lastOutputAt: this.lastOutputAt,
|
|
1631
1180
|
}),
|
|
1632
1181
|
activeModal,
|
|
1633
|
-
providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
|
|
1182
|
+
providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
|
|
1183
|
+
errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
|
|
1184
|
+
? (parsed as any).errorMessage.trim()
|
|
1185
|
+
: undefined,
|
|
1186
|
+
errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
|
|
1187
|
+
? (parsed as any).errorReason.trim()
|
|
1188
|
+
: undefined,
|
|
1634
1189
|
...(bufferState ? { bufferState } : {}),
|
|
1635
1190
|
...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
|
|
1636
1191
|
? { transcriptAuthority: (parsed as any).transcriptAuthority }
|
|
@@ -1642,13 +1197,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1642
1197
|
|
|
1643
1198
|
this.parsedStatusCache = {
|
|
1644
1199
|
responseBuffer: this.responseBuffer,
|
|
1645
|
-
currentTurnScope: this.currentTurnScope,
|
|
1200
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
1646
1201
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1647
1202
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1648
1203
|
accumulatedRawBufferKey,
|
|
1649
1204
|
screenText: parseScreenText,
|
|
1650
|
-
currentStatus: this.currentStatus,
|
|
1651
|
-
activeModal: this.activeModal,
|
|
1205
|
+
currentStatus: this.engine.currentStatus,
|
|
1206
|
+
activeModal: this.engine.activeModal,
|
|
1652
1207
|
cliName: this.cliName,
|
|
1653
1208
|
result,
|
|
1654
1209
|
};
|
|
@@ -1656,22 +1211,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1656
1211
|
}
|
|
1657
1212
|
|
|
1658
1213
|
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
1214
|
const input = buildCliParseInput({
|
|
1664
1215
|
accumulatedBuffer: this.accumulatedBuffer,
|
|
1665
1216
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1666
1217
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1667
1218
|
terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
|
|
1219
|
+
workingDir: this.workingDir,
|
|
1220
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1221
|
+
historySessionId: this.providerSessionId || undefined,
|
|
1668
1222
|
baseMessages: [],
|
|
1669
1223
|
partialResponse: this.responseBuffer,
|
|
1670
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
1671
|
-
scope: this.currentTurnScope,
|
|
1224
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1225
|
+
scope: this.engine.currentTurnScope,
|
|
1672
1226
|
runtimeSettings: this.runtimeSettings,
|
|
1227
|
+
spawnAt: this.spawnAt,
|
|
1673
1228
|
});
|
|
1674
|
-
return await Promise.resolve(this.
|
|
1229
|
+
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
1675
1230
|
...input,
|
|
1676
1231
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1677
1232
|
}));
|
|
@@ -1683,7 +1238,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1683
1238
|
|
|
1684
1239
|
/** Whether this adapter has CLI scripts loaded */
|
|
1685
1240
|
hasCliScripts(): boolean {
|
|
1686
|
-
return
|
|
1241
|
+
return this.runner.hasDetectStatus();
|
|
1687
1242
|
}
|
|
1688
1243
|
|
|
1689
1244
|
/**
|
|
@@ -1692,24 +1247,29 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1692
1247
|
*/
|
|
1693
1248
|
async resolveAction(data: any): Promise<void> {
|
|
1694
1249
|
let promptText = '';
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1250
|
+
try {
|
|
1251
|
+
promptText = this.runner.invokeByName('resolveAction', data);
|
|
1252
|
+
} catch {
|
|
1253
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
|
|
1254
|
+
return;
|
|
1701
1255
|
}
|
|
1702
1256
|
if (!promptText) {
|
|
1703
|
-
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not
|
|
1257
|
+
LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
|
|
1704
1258
|
return;
|
|
1705
1259
|
}
|
|
1706
1260
|
await this.sendMessage(promptText);
|
|
1707
1261
|
}
|
|
1708
1262
|
|
|
1263
|
+
async setInteractivePromptResponse(_response: InteractivePromptResponse): Promise<void> {
|
|
1264
|
+
// Legacy TUI providers do not currently expose an interactive prompt
|
|
1265
|
+
// protocol. Spec-backed claude-cli implements the first real wiring.
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1709
1268
|
private isSubmitStuck(normalizedPromptSnippet: string): boolean {
|
|
1710
|
-
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
|
|
1711
|
-
if (this.hasActionableApproval()) return false;
|
|
1712
|
-
|
|
1269
|
+
if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
|
|
1270
|
+
if (this.engine.hasActionableApproval()) return false;
|
|
1271
|
+
// If there's already meaningful response content beyond the echoed prompt, not stuck
|
|
1272
|
+
if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
|
|
1713
1273
|
const screenText = this.terminalScreen.getText();
|
|
1714
1274
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
|
|
1715
1275
|
const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
|
|
@@ -1718,25 +1278,97 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1718
1278
|
return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
|
|
1719
1279
|
}
|
|
1720
1280
|
|
|
1721
|
-
private
|
|
1281
|
+
private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
|
|
1282
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
1283
|
+
if (!raw) return false;
|
|
1284
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
1285
|
+
if (!normalizedPrompt) return true;
|
|
1286
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
1287
|
+
if (!normalizedBuffer) return false;
|
|
1288
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
1289
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
1290
|
+
const remainder = normalizedBuffer
|
|
1291
|
+
.slice(normalizedPrompt.length)
|
|
1292
|
+
.replace(/[─═\-]+/g, '')
|
|
1293
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
1294
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
1295
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
1296
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
1297
|
+
.replace(/esctointerrupt/gi, '')
|
|
1298
|
+
.replace(/❯/g, '')
|
|
1299
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
1300
|
+
.trim();
|
|
1301
|
+
return remainder.length > 0;
|
|
1302
|
+
}
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** Serialize `data` onto the per-session PTY write chain. Returns a promise
|
|
1307
|
+
* that resolves (or rejects) with THIS write's own outcome, so callers keep
|
|
1308
|
+
* their existing "await my write completion" semantics. The underlying chain
|
|
1309
|
+
* link swallows its own error before releasing the next write, so a failed
|
|
1310
|
+
* write surfaces to its caller but never wedges subsequent writes behind a
|
|
1311
|
+
* permanently-rejected tail (FIFO order preserved). */
|
|
1312
|
+
private writeToPty(data: string): Promise<void> {
|
|
1313
|
+
const run = this.ptyWriteChain.then(() => this.doWriteToPty(data));
|
|
1314
|
+
// Keep the tail resolved so the next writeToPty() always chains onto a
|
|
1315
|
+
// settled promise (never a rejected one). The caller still sees run's
|
|
1316
|
+
// rejection; only the shared chain is insulated from it.
|
|
1317
|
+
this.ptyWriteChain = run.catch(() => {});
|
|
1318
|
+
return run;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
private async doWriteToPty(data: string): Promise<void> {
|
|
1722
1322
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1323
|
+
// win32 ConPTY paced write: a single unbounded write beyond ~1KB overflows
|
|
1324
|
+
// the console input pipe and drops LEADING bytes (the "long message gets
|
|
1325
|
+
// truncated, head lost / tail kept" failure). Split a large payload into
|
|
1326
|
+
// bounded, surrogate-safe chunks written with a short gap so the console
|
|
1327
|
+
// reader keeps up. Small payloads (the common case — short prompts, lone
|
|
1328
|
+
// submit keys) still go out in a single write.
|
|
1329
|
+
//
|
|
1330
|
+
// The submit key, when present, is the TAIL of `data` (callers pass
|
|
1331
|
+
// `body + sendKey` for the atomic-submit paths). Because we chunk the
|
|
1332
|
+
// combined string, the submit key always rides in the SAME final write as
|
|
1333
|
+
// the body's tail — the win32 invariant that ConPTY recognizes it as a
|
|
1334
|
+
// submit — and is never emitted before the whole body has been written
|
|
1335
|
+
// (no partial-body submit). Body-only writes (wait_for_echo strategy) have
|
|
1336
|
+
// their submit key sent separately by the caller afterwards, unchanged.
|
|
1337
|
+
if (process.platform === 'win32' && shouldChunkWin32Write(data.length)) {
|
|
1338
|
+
await this.writeWin32Chunked(data);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1723
1341
|
await this.ptyProcess.write(data);
|
|
1724
1342
|
}
|
|
1725
1343
|
|
|
1344
|
+
/** Write `data` to the PTY in bounded, surrogate-safe chunks with a short
|
|
1345
|
+
* inter-chunk gap (win32 paced write). Awaits each chunk's write and the gap
|
|
1346
|
+
* so the returned promise resolves only after the FINAL chunk (carrying any
|
|
1347
|
+
* trailing submit key) has been written. */
|
|
1348
|
+
private async writeWin32Chunked(data: string): Promise<void> {
|
|
1349
|
+
const chunks = chunkPreservingSurrogates(data, WIN32_PTY_WRITE_CHUNK_CHARS);
|
|
1350
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
1351
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1352
|
+
await this.ptyProcess.write(chunks[i]);
|
|
1353
|
+
if (i + 1 < chunks.length) {
|
|
1354
|
+
await new Promise<void>(resolve => setTimeout(resolve, WIN32_PTY_WRITE_CHUNK_GAP_MS));
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1726
1359
|
private resetPendingSendState(reason: string): void {
|
|
1727
|
-
this.isWaitingForResponse = false;
|
|
1728
1360
|
this.responseBuffer = '';
|
|
1729
|
-
this.currentTurnScope = null;
|
|
1730
|
-
this.submitPendingUntil = 0;
|
|
1731
|
-
this.clearIdleFinishCandidate(reason);
|
|
1732
1361
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1733
|
-
|
|
1734
|
-
|
|
1362
|
+
this.engine.resetActiveTurnState();
|
|
1363
|
+
this.engine.clearIdleFinishCandidate(reason);
|
|
1735
1364
|
}
|
|
1736
1365
|
|
|
1737
1366
|
private commitSendUserTurn(state: SendMessageState): void {
|
|
1738
1367
|
if (state.didCommitUserTurn) return;
|
|
1739
1368
|
state.didCommitUserTurn = true;
|
|
1369
|
+
// The first turn has now been written atomically (win32-safe); later turns
|
|
1370
|
+
// can use the normal echo-gated path now that the input layer is proven live.
|
|
1371
|
+
this.firstTurnSent = true;
|
|
1740
1372
|
}
|
|
1741
1373
|
|
|
1742
1374
|
private armResponseTimeout(): void {
|
|
@@ -1748,42 +1380,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1748
1380
|
}
|
|
1749
1381
|
this.responseTimeout = setTimeout(() => {
|
|
1750
1382
|
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
|
-
});
|
|
1383
|
+
if (!this.engine.isWaitingForResponse) return;
|
|
1768
1384
|
|
|
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
|
|
1385
|
+
// maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
|
|
1773
1386
|
// normal settled parser instead and keep the turn open unless the provider
|
|
1774
1387
|
// 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
|
-
});
|
|
1388
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
1389
|
+
|
|
1390
|
+
if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
|
|
1787
1391
|
this.armResponseTimeout();
|
|
1788
1392
|
}
|
|
1789
1393
|
}, timeoutMs);
|
|
@@ -1795,37 +1399,42 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1795
1399
|
});
|
|
1796
1400
|
}
|
|
1797
1401
|
|
|
1402
|
+
// WIN32-READY-HOLD: choose the retry write for a stuck prompt. When the FIRST turn
|
|
1403
|
+
// is stuck on win32 — the premature-ready swallow window — the prompt text itself
|
|
1404
|
+
// may have been partially eaten by a not-yet-live ConPTY input layer, so re-sending
|
|
1405
|
+
// a bare CR keeps hitting nothing. Re-type the whole `text + sendKey` atomically
|
|
1406
|
+
// once so the input layer (now live) receives a self-contained, submit-coupled
|
|
1407
|
+
// write. All other cases keep the cheap bare-CR retry (the prompt is fully echoed
|
|
1408
|
+
// and only the Enter is missing).
|
|
1409
|
+
private writeStuckRetry(state: SendMessageState, mode: string): void {
|
|
1410
|
+
const retypeFirstTurn = process.platform === 'win32' && state.isFirstTurn;
|
|
1411
|
+
if (retypeFirstTurn) {
|
|
1412
|
+
LOG.info('CLI', `[${this.cliType}] ${mode}: re-typing full prompt atomically (win32 first-turn swallow recovery)`);
|
|
1413
|
+
void this.writeToPty(state.text + this.sendKey).catch((error) => {
|
|
1414
|
+
LOG.warn('CLI', `[${this.cliType}] ${mode} re-type write failed: ${error?.message || error}`);
|
|
1415
|
+
});
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
this.writeSubmitKeyForRetry(mode);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1798
1421
|
private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
|
|
1799
1422
|
this.submitRetryTimer = null;
|
|
1800
1423
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1801
|
-
|
|
1802
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1424
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1803
1425
|
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; }
|
|
1426
|
+
this.writeStuckRetry(state, 'submit_retry');
|
|
1427
|
+
if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
|
|
1812
1428
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
|
|
1813
1429
|
}
|
|
1814
1430
|
|
|
1815
1431
|
private retryImmediateSubmitIfStuck(state: SendMessageState): void {
|
|
1816
1432
|
this.submitRetryTimer = null;
|
|
1817
1433
|
if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
|
|
1818
|
-
|
|
1819
|
-
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1434
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1820
1435
|
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;
|
|
1436
|
+
this.writeStuckRetry(state, 'immediate_retry');
|
|
1437
|
+
this.engine.submitRetryUsed = true;
|
|
1829
1438
|
}
|
|
1830
1439
|
|
|
1831
1440
|
private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
|
|
@@ -1833,13 +1442,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1833
1442
|
completion.resolveOnce();
|
|
1834
1443
|
return;
|
|
1835
1444
|
}
|
|
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
|
-
});
|
|
1445
|
+
this.engine.submitPendingUntil = 0;
|
|
1843
1446
|
void this.writeToPty(this.sendKey).then(() => {
|
|
1844
1447
|
this.commitSendUserTurn(state);
|
|
1845
1448
|
this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
|
|
@@ -1849,13 +1452,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1849
1452
|
}
|
|
1850
1453
|
|
|
1851
1454
|
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
|
-
});
|
|
1455
|
+
this.engine.submitPendingUntil = 0;
|
|
1859
1456
|
void this.writeToPty(state.text + this.sendKey).then(() => {
|
|
1860
1457
|
this.commitSendUserTurn(state);
|
|
1861
1458
|
this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
|
|
@@ -1899,7 +1496,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1899
1496
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
1900
1497
|
screenText: summarizeCliTraceText(screenText, 1000),
|
|
1901
1498
|
};
|
|
1902
|
-
|
|
1499
|
+
LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
|
|
1500
|
+
|
|
1903
1501
|
if (this.requirePromptEchoBeforeSubmit) {
|
|
1904
1502
|
// At this point the prompt text write already completed. Rejecting without
|
|
1905
1503
|
// a submit key can leave the delegated CLI with an unsent prompt sitting at
|
|
@@ -1924,12 +1522,153 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1924
1522
|
), 50);
|
|
1925
1523
|
}
|
|
1926
1524
|
|
|
1927
|
-
async sendMessage(text: string): Promise<void> {
|
|
1525
|
+
async sendMessage(text: string, options: { force?: boolean; meshTaskId?: string } = {}): Promise<void> {
|
|
1526
|
+
if (options.force === true) {
|
|
1527
|
+
await this.forceSendMessage(text, options.meshTaskId);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
await this.sendMessageNow(text, true, options.meshTaskId);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
async forceSendMessage(text: string, meshTaskId?: string): Promise<void> {
|
|
1534
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1535
|
+
const content = String(text || '');
|
|
1536
|
+
if (!content.trim()) return;
|
|
1537
|
+
// ARCH-REFACTOR R1: a force-send (mesh coordinator dispatch / reconcile
|
|
1538
|
+
// redelivery) bypasses the normal turnScope pipeline (raw PTY write), so there is
|
|
1539
|
+
// no turnScope to carry the taskId. Bind it directly on the engine so the
|
|
1540
|
+
// resulting turn's completion is still attributed to the right task.
|
|
1541
|
+
if (typeof meshTaskId === 'string' && meshTaskId.trim()) {
|
|
1542
|
+
this.engine.currentTurnTaskId = meshTaskId;
|
|
1543
|
+
}
|
|
1544
|
+
// Modal-park guard (defense-in-depth — the primary guard is at the
|
|
1545
|
+
// cli-provider-instance force-forward chokepoint). A force-write writes raw
|
|
1546
|
+
// keystrokes into the PTY, bypassing the busy send-guard. If the session is
|
|
1547
|
+
// parked on a tool-consent modal, the modal's key handler eats those bytes
|
|
1548
|
+
// and silently resolves an approval the user never made. Hold the write and
|
|
1549
|
+
// let the mesh reconcile loop redeliver once the modal is resolved. We only
|
|
1550
|
+
// hold for an actionable approval modal; plain generating is still force-written
|
|
1551
|
+
// (that is the deadlock the force path exists to break).
|
|
1552
|
+
if (this.engine.currentStatus === 'waiting_approval' || this.engine.hasActionableApproval()) {
|
|
1553
|
+
LOG.info('CLI', `[${this.cliType}] force-send held — session parked on approval modal (status=${this.engine.currentStatus})`);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
|
|
1557
|
+
// Settle, then submit atomically. The previous split-write fix
|
|
1558
|
+
// (write text → settle → write a separate '\r') regressed win32:
|
|
1559
|
+
// ConPTY does not treat a lone CR arriving in its own PTY chunk after a
|
|
1560
|
+
// delay as a submit key, so the Enter was swallowed and the prompt sat
|
|
1561
|
+
// typed-but-unsent. Instead, honor a fixed settle gap so the TUI input
|
|
1562
|
+
// handler is in its input-accepting state, then write `content + sendKey`
|
|
1563
|
+
// as ONE PTY write — the same atomic submit the normal `immediate`
|
|
1564
|
+
// strategy uses. Keeping the Enter in the same write unit as the text is
|
|
1565
|
+
// the invariant that makes win32 ConPTY recognize the submit, and it
|
|
1566
|
+
// also makes the original inject↔submit race impossible (the Enter can
|
|
1567
|
+
// never be separated from the text it submits). The settle still guards
|
|
1568
|
+
// the original concern — that a force-dispatch injected into a freshly
|
|
1569
|
+
// idle session lands before the TUI is ready to accept input.
|
|
1570
|
+
await this.waitForForceSubmitSettle();
|
|
1571
|
+
await this.writeToPty(content + this.sendKey);
|
|
1572
|
+
this.onStatusChange?.();
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
private async waitForForceSubmitSettle(): Promise<void> {
|
|
1576
|
+
// A fixed minimum gap so the input handler is ready to accept the paste.
|
|
1577
|
+
// We deliberately do NOT echo-gate here: the submit key is written in the
|
|
1578
|
+
// same atomic PTY write as the text (forceSendMessage), so there is no
|
|
1579
|
+
// separate Enter that could race the echo, and on win32 the echo gate
|
|
1580
|
+
// never reliably matched anyway.
|
|
1581
|
+
await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_SETTLE_MS));
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
private enqueuePendingOutboundMessage(text: string, reason: string, meshTaskId?: string): void {
|
|
1585
|
+
const content = String(text || '');
|
|
1586
|
+
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
1587
|
+
if (duplicate) {
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
const queuedAt = Date.now();
|
|
1591
|
+
const message: PendingOutboundMessage = {
|
|
1592
|
+
id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
|
|
1593
|
+
role: 'user',
|
|
1594
|
+
content,
|
|
1595
|
+
queuedAt,
|
|
1596
|
+
source: 'sendMessage',
|
|
1597
|
+
...(typeof meshTaskId === 'string' && meshTaskId.trim() ? { meshTaskId } : {}),
|
|
1598
|
+
};
|
|
1599
|
+
this.pendingOutboundQueue.push(message);
|
|
1600
|
+
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
1601
|
+
this.onStatusChange?.();
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
1605
|
+
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
1606
|
+
if (this.engine.hasActionableApproval()) return null;
|
|
1607
|
+
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1608
|
+
? String(parsedStatusBeforeSend.status)
|
|
1609
|
+
: '';
|
|
1610
|
+
const hasFinalAssistant = (p: any) => {
|
|
1611
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
1612
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
1613
|
+
};
|
|
1614
|
+
if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
|
|
1615
|
+
if (this.engine.currentStatus === 'generating') return 'current_status_generating';
|
|
1616
|
+
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating') {
|
|
1617
|
+
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1618
|
+
const parsedHasActionableModal = Boolean(
|
|
1619
|
+
parsedModal
|
|
1620
|
+
&& Array.isArray(parsedModal.buttons)
|
|
1621
|
+
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1622
|
+
);
|
|
1623
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1624
|
+
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1625
|
+
&& !this.engine.isWaitingForResponse
|
|
1626
|
+
&& !this.engine.currentTurnScope
|
|
1627
|
+
&& !this.engine.hasActionableApproval()
|
|
1628
|
+
&& !parsedHasActionableModal;
|
|
1629
|
+
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
1630
|
+
}
|
|
1631
|
+
if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
private schedulePendingOutboundFlush(delayMs = 0): void {
|
|
1636
|
+
if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
|
|
1637
|
+
this.pendingOutboundFlushTimer = setTimeout(() => {
|
|
1638
|
+
this.pendingOutboundFlushTimer = null;
|
|
1639
|
+
void this.flushPendingOutboundQueue();
|
|
1640
|
+
}, Math.max(0, delayMs));
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
private async flushPendingOutboundQueue(): Promise<void> {
|
|
1644
|
+
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
1645
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
|
|
1646
|
+
this.pendingOutboundFlushInFlight = true;
|
|
1647
|
+
try {
|
|
1648
|
+
while (this.pendingOutboundQueue.length > 0) {
|
|
1649
|
+
if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
|
|
1650
|
+
const next = this.pendingOutboundQueue[0];
|
|
1651
|
+
try {
|
|
1652
|
+
await this.sendMessageNow(next.content, false, next.meshTaskId);
|
|
1653
|
+
this.pendingOutboundQueue.shift();
|
|
1654
|
+
this.onStatusChange?.();
|
|
1655
|
+
} catch (error: any) {
|
|
1656
|
+
LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
|
|
1657
|
+
this.schedulePendingOutboundFlush(1000);
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
} finally {
|
|
1662
|
+
this.pendingOutboundFlushInFlight = false;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
private async sendMessageNow(text: string, allowQueue: boolean, meshTaskId?: string): Promise<void> {
|
|
1928
1667
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1929
1668
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
1930
1669
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
1931
|
-
&& this.isWaitingForResponse
|
|
1932
|
-
&& !this.hasActionableApproval();
|
|
1670
|
+
&& this.engine.isWaitingForResponse
|
|
1671
|
+
&& !this.engine.hasActionableApproval();
|
|
1933
1672
|
if (this.startupParseGate) {
|
|
1934
1673
|
const deadline = Date.now() + 10000;
|
|
1935
1674
|
while (this.startupParseGate && Date.now() < deadline) {
|
|
@@ -1937,76 +1676,106 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1937
1676
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1938
1677
|
}
|
|
1939
1678
|
}
|
|
1679
|
+
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
1680
|
+
? (() => {
|
|
1681
|
+
try {
|
|
1682
|
+
return this.getScriptParsedStatus?.() || null;
|
|
1683
|
+
} catch {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
})()
|
|
1687
|
+
: null;
|
|
1688
|
+
const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
|
|
1689
|
+
if (allowQueue && queueReason) {
|
|
1690
|
+
this.enqueuePendingOutboundMessage(text, queueReason, meshTaskId);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1940
1693
|
if (!allowInterventionPrompt) {
|
|
1941
1694
|
await this.waitForInteractivePrompt();
|
|
1942
1695
|
}
|
|
1943
1696
|
if (!this.ready) {
|
|
1944
1697
|
this.resolveStartupState('send_precheck');
|
|
1945
|
-
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1698
|
+
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
1946
1699
|
this.ready = true;
|
|
1947
1700
|
this.startupParseGate = false;
|
|
1701
|
+
this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1948
1702
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
1949
1703
|
}
|
|
1950
1704
|
}
|
|
1951
|
-
if (!this.ready)
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1705
|
+
if (!this.ready) {
|
|
1706
|
+
// Readiness barrier (queue-until-ready). A task dispatched the instant a
|
|
1707
|
+
// freshly-spawned session is launched can arrive BEFORE the PTY prints its
|
|
1708
|
+
// interactive prompt (this.ready flips ~2-6s later). Previously this threw
|
|
1709
|
+
// "not ready" and the delegated-task delivery promise requeued the task,
|
|
1710
|
+
// which on win32 raced the auto-launch cooldown and could strand the worker
|
|
1711
|
+
// idle with no work (the "first big message lost" failure). Instead, when the
|
|
1712
|
+
// caller allows queueing, BUFFER the message in the pending-outbound queue and
|
|
1713
|
+
// return — the startup-settle path flips this.ready and flushes the queue once
|
|
1714
|
+
// the prompt is actually up (see resolveStartupState → flushPendingOutboundQueue),
|
|
1715
|
+
// so the message is delivered late rather than dropped. A non-queueable caller
|
|
1716
|
+
// (e.g. an internal flush) still throws so it isn't silently swallowed.
|
|
1717
|
+
if (allowQueue) {
|
|
1718
|
+
this.enqueuePendingOutboundMessage(text, 'not_ready_pending_prompt', meshTaskId);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
1722
|
+
}
|
|
1961
1723
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1962
1724
|
? String(parsedStatusBeforeSend.status)
|
|
1963
1725
|
: '';
|
|
1964
|
-
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating')) {
|
|
1726
|
+
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating')) {
|
|
1965
1727
|
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
1966
1728
|
const parsedHasActionableModal = Boolean(
|
|
1967
1729
|
parsedModal
|
|
1968
1730
|
&& Array.isArray(parsedModal.buttons)
|
|
1969
1731
|
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
1970
1732
|
);
|
|
1971
|
-
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
1733
|
+
const terminalLooksIdle = this.engine.currentStatus === 'idle'
|
|
1972
1734
|
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
1973
|
-
&& !this.isWaitingForResponse
|
|
1974
|
-
&& !this.currentTurnScope
|
|
1975
|
-
&& !this.hasActionableApproval()
|
|
1735
|
+
&& !this.engine.isWaitingForResponse
|
|
1736
|
+
&& !this.engine.currentTurnScope
|
|
1737
|
+
&& !this.engine.hasActionableApproval()
|
|
1976
1738
|
&& !parsedHasActionableModal;
|
|
1977
1739
|
if (!terminalLooksIdle) {
|
|
1740
|
+
if (allowQueue) {
|
|
1741
|
+
this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`, meshTaskId);
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1978
1744
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1979
1745
|
}
|
|
1980
1746
|
}
|
|
1981
|
-
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1982
|
-
|
|
1747
|
+
if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1748
|
+
const snap = this.getSnapshot();
|
|
1749
|
+
if (
|
|
1750
|
+
!this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
|
|
1751
|
+
&& !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
|
|
1752
|
+
) {
|
|
1753
|
+
if (allowQueue) {
|
|
1754
|
+
this.enqueuePendingOutboundMessage(text, 'waiting_for_response', meshTaskId);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1983
1757
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1984
1758
|
}
|
|
1985
1759
|
}
|
|
1986
|
-
this.isWaitingForResponse = true;
|
|
1987
1760
|
this.responseBuffer = '';
|
|
1988
|
-
|
|
1989
|
-
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1990
|
-
this.clearIdleFinishCandidate('send_message');
|
|
1991
|
-
this.currentTurnScope = {
|
|
1761
|
+
const turnScope: TurnParseScope = {
|
|
1992
1762
|
prompt: text,
|
|
1993
1763
|
startedAt: Date.now(),
|
|
1994
1764
|
bufferStart: this.accumulatedBuffer.length,
|
|
1995
1765
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
1766
|
+
// ARCH-REFACTOR R1: bind this turn to its mesh task. engine.onTurnStarted
|
|
1767
|
+
// copies this into currentTurnTaskId so the turn's completion event carries
|
|
1768
|
+
// the right id even if a later task overwrites the session scalar meanwhile.
|
|
1769
|
+
...(typeof meshTaskId === 'string' && meshTaskId.trim() ? { taskId: meshTaskId } : {}),
|
|
1996
1770
|
};
|
|
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);
|
|
1771
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
2006
1772
|
if (this.submitRetryTimer) {
|
|
2007
1773
|
clearTimeout(this.submitRetryTimer);
|
|
2008
1774
|
this.submitRetryTimer = null;
|
|
2009
1775
|
}
|
|
1776
|
+
this.engine.onTurnStarted(turnScope);
|
|
1777
|
+
this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1778
|
+
const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
|
|
2010
1779
|
const estimatedLines = estimatePromptDisplayLines(text);
|
|
2011
1780
|
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
2012
1781
|
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
@@ -2018,13 +1787,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2018
1787
|
maxEchoWaitMs,
|
|
2019
1788
|
retryDelayMs,
|
|
2020
1789
|
didCommitUserTurn: false,
|
|
1790
|
+
// Capture BEFORE the send commits — commitSendUserTurn flips firstTurnSent.
|
|
1791
|
+
isFirstTurn: !this.firstTurnSent,
|
|
2021
1792
|
};
|
|
2022
|
-
|
|
2023
|
-
clearTimeout(this.settleTimer);
|
|
2024
|
-
this.settleTimer = null;
|
|
2025
|
-
}
|
|
2026
|
-
this.responseEpoch += 1;
|
|
2027
|
-
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
1793
|
+
this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
2028
1794
|
await new Promise<void>((resolve, reject) => {
|
|
2029
1795
|
let resolved = false;
|
|
2030
1796
|
const completion: SendMessageCompletion = {
|
|
@@ -2041,30 +1807,34 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2041
1807
|
},
|
|
2042
1808
|
};
|
|
2043
1809
|
|
|
2044
|
-
|
|
1810
|
+
// WIN32-READY-HOLD: the very first turn after startup is the one exposed to
|
|
1811
|
+
// the premature-ready swallow (ready released before win32 ConPTY input is
|
|
1812
|
+
// live). Force it through the atomic content+sendKey single write so the
|
|
1813
|
+
// submit CR can never be separated from the text it submits — the same path
|
|
1814
|
+
// the `immediate` strategy already uses. Restricted to win32 + the first
|
|
1815
|
+
// turn so Mac/linux echo-gated behavior and all later turns are unchanged.
|
|
1816
|
+
const useAtomicFirstTurn = this.submitStrategy === 'immediate'
|
|
1817
|
+
|| (process.platform === 'win32' && sendState.isFirstTurn);
|
|
1818
|
+
if (useAtomicFirstTurn) {
|
|
2045
1819
|
this.submitImmediatePrompt(sendState, completion);
|
|
2046
1820
|
return;
|
|
2047
1821
|
}
|
|
2048
1822
|
|
|
2049
1823
|
if (submitDelayMs > 0) {
|
|
2050
|
-
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1824
|
+
this.engine.submitPendingUntil = Date.now() + submitDelayMs;
|
|
2051
1825
|
}
|
|
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
1826
|
const submitStartedAt = Date.now();
|
|
2059
1827
|
void this.writeToPty(text).then(
|
|
2060
1828
|
() => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
|
|
2061
1829
|
completion.rejectOnce,
|
|
2062
1830
|
);
|
|
2063
1831
|
});
|
|
1832
|
+
// Schedule settle after successful send
|
|
1833
|
+
this.engine.scheduleSettle();
|
|
2064
1834
|
}
|
|
2065
1835
|
|
|
2066
1836
|
getPartialResponse(): string {
|
|
2067
|
-
if (!this.isWaitingForResponse) return '';
|
|
1837
|
+
if (!this.engine.isWaitingForResponse) return '';
|
|
2068
1838
|
return this.responseBuffer;
|
|
2069
1839
|
}
|
|
2070
1840
|
|
|
@@ -2077,10 +1847,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2077
1847
|
cliType: this.cliType,
|
|
2078
1848
|
cliName: this.cliName,
|
|
2079
1849
|
workingDir: this.workingDir,
|
|
2080
|
-
currentStatus: this.currentStatus,
|
|
1850
|
+
currentStatus: this.engine.currentStatus,
|
|
2081
1851
|
ready: this.ready,
|
|
2082
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2083
|
-
activeModal: this.activeModal,
|
|
1852
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
1853
|
+
activeModal: this.engine.activeModal,
|
|
2084
1854
|
parseErrorMessage: this.parseErrorMessage,
|
|
2085
1855
|
messageCounts: {
|
|
2086
1856
|
parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
|
|
@@ -2106,10 +1876,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2106
1876
|
lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
|
|
2107
1877
|
},
|
|
2108
1878
|
parser: {
|
|
2109
|
-
scriptNames:
|
|
2110
|
-
traceSessionId: this.
|
|
2111
|
-
traceSeq: this.
|
|
2112
|
-
currentTurnScope: this.currentTurnScope,
|
|
1879
|
+
scriptNames: this.runner.getScriptNames(),
|
|
1880
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
1881
|
+
traceSeq: this.engine.getTraceEntries().length,
|
|
1882
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2113
1883
|
parsedStatusCache: parsedResult
|
|
2114
1884
|
? {
|
|
2115
1885
|
id: parsedResult.id,
|
|
@@ -2122,26 +1892,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2122
1892
|
activeModal: parsedResult.activeModal,
|
|
2123
1893
|
}
|
|
2124
1894
|
: null,
|
|
2125
|
-
pendingScriptStatus: this.pendingScriptStatus,
|
|
2126
|
-
pendingScriptStatusSince: this.pendingScriptStatusSince,
|
|
1895
|
+
pendingScriptStatus: this.engine.pendingScriptStatus,
|
|
1896
|
+
pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
|
|
2127
1897
|
},
|
|
2128
1898
|
runtimeMetadata: this.getRuntimeMetadata(),
|
|
2129
|
-
statusHistory: this.
|
|
2130
|
-
traceEntries: this.
|
|
1899
|
+
statusHistory: this.engine.getStatusHistory().slice(-80),
|
|
1900
|
+
traceEntries: this.engine.getTraceEntries().slice(-120),
|
|
2131
1901
|
timing: {
|
|
2132
1902
|
spawnAt: this.spawnAt,
|
|
2133
1903
|
startupFirstOutputAt: this.startupFirstOutputAt,
|
|
2134
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2135
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2136
|
-
responseEpoch: this.responseEpoch,
|
|
1904
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
1905
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
1906
|
+
responseEpoch: this.engine.responseEpoch,
|
|
2137
1907
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2138
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1908
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2139
1909
|
},
|
|
2140
1910
|
finish: {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
submitRetryPromptSnippet: this.submitRetryPromptSnippet,
|
|
1911
|
+
finishRetryCount: this.engine.finishRetryCount,
|
|
1912
|
+
submitRetryUsed: this.engine.submitRetryUsed,
|
|
1913
|
+
submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
|
|
2145
1914
|
},
|
|
2146
1915
|
};
|
|
2147
1916
|
}
|
|
@@ -2151,7 +1920,48 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2151
1920
|
return this.ptyProcess.getMetadata();
|
|
2152
1921
|
}
|
|
2153
1922
|
|
|
1923
|
+
/**
|
|
1924
|
+
* Launch metadata for the dashboard Session info panel. Re-derives the spawn plan
|
|
1925
|
+
* (pure — same inputs the live PTY was spawned with) so the dashboard sees the
|
|
1926
|
+
* resolved binary, full arg vector, and cwd without us having to persist it.
|
|
1927
|
+
* extraEnv values are intentionally dropped (keys only) so secrets passed at
|
|
1928
|
+
* launch time never reach the dashboard.
|
|
1929
|
+
*/
|
|
1930
|
+
getLaunchInfo(): CliLaunchInfo {
|
|
1931
|
+
let command: string | undefined;
|
|
1932
|
+
let args: string[] = [...this.extraArgs];
|
|
1933
|
+
try {
|
|
1934
|
+
const plan = resolveCliSpawnPlan({
|
|
1935
|
+
provider: this.provider,
|
|
1936
|
+
runtimeSettings: this.runtimeSettings,
|
|
1937
|
+
workingDir: this.workingDir,
|
|
1938
|
+
extraArgs: this.extraArgs,
|
|
1939
|
+
extraEnv: this.extraEnv,
|
|
1940
|
+
});
|
|
1941
|
+
command = plan.binaryPath;
|
|
1942
|
+
args = plan.allArgs;
|
|
1943
|
+
} catch {
|
|
1944
|
+
// Spawn plan resolution can throw before the binary is resolvable
|
|
1945
|
+
// (e.g. CLI not installed). Fall back to the raw extra args so the
|
|
1946
|
+
// panel still shows what it can rather than failing the whole call.
|
|
1947
|
+
}
|
|
1948
|
+
return {
|
|
1949
|
+
command,
|
|
1950
|
+
args,
|
|
1951
|
+
extraArgs: [...this.extraArgs],
|
|
1952
|
+
cwd: this.workingDir,
|
|
1953
|
+
extraEnvKeys: Object.keys(this.extraEnv || {}),
|
|
1954
|
+
providerSessionId: this.providerSessionId || undefined,
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
|
|
2154
1958
|
updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
|
|
1959
|
+
const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
|
|
1960
|
+
? meta.providerSessionId.trim()
|
|
1961
|
+
: '';
|
|
1962
|
+
if (nextProviderSessionId) {
|
|
1963
|
+
this.providerSessionId = nextProviderSessionId;
|
|
1964
|
+
}
|
|
2155
1965
|
if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
|
|
2156
1966
|
this.ptyProcess.updateMeta(meta, replace);
|
|
2157
1967
|
}
|
|
@@ -2172,7 +1982,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2172
1982
|
this.timeouts.shutdownGrace,
|
|
2173
1983
|
typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
|
|
2174
1984
|
);
|
|
2175
|
-
const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
|
|
1985
|
+
const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
|
|
2176
1986
|
|
|
2177
1987
|
try {
|
|
2178
1988
|
if (wasProcessing) {
|
|
@@ -2210,7 +2020,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2210
2020
|
return new Promise((resolve) => {
|
|
2211
2021
|
const startedAt = Date.now();
|
|
2212
2022
|
const timer = setInterval(() => {
|
|
2213
|
-
if (!this.ptyProcess || this.currentStatus === 'stopped') {
|
|
2023
|
+
if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
|
|
2214
2024
|
clearInterval(timer);
|
|
2215
2025
|
resolve(true);
|
|
2216
2026
|
return;
|
|
@@ -2224,18 +2034,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2224
2034
|
}
|
|
2225
2035
|
|
|
2226
2036
|
shutdown(): void {
|
|
2227
|
-
this.clearIdleFinishCandidate('shutdown');
|
|
2037
|
+
this.engine.clearIdleFinishCandidate('shutdown');
|
|
2228
2038
|
this.clearAllTimers();
|
|
2229
2039
|
this.pendingOutputParseChunks = [];
|
|
2230
2040
|
this.pendingTerminalQueryTail = '';
|
|
2231
2041
|
this.ptyOutputChunks = [];
|
|
2232
|
-
this.
|
|
2042
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2043
|
+
this.pendingOutboundQueue = [];
|
|
2044
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2233
2045
|
if (this.ptyProcess) {
|
|
2234
2046
|
this.ptyProcess.write('\x03');
|
|
2235
2047
|
setTimeout(() => {
|
|
2236
2048
|
try { this.ptyProcess?.kill(); } catch { }
|
|
2237
2049
|
this.ptyProcess = null;
|
|
2238
|
-
this.setStatus('stopped', 'stop_cmd');
|
|
2050
|
+
this.engine.setStatus('stopped', 'stop_cmd');
|
|
2239
2051
|
this.ready = false;
|
|
2240
2052
|
this.startupParseGate = false;
|
|
2241
2053
|
this.spawnAt = 0;
|
|
@@ -2245,12 +2057,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2245
2057
|
}
|
|
2246
2058
|
|
|
2247
2059
|
detach(): void {
|
|
2248
|
-
this.clearIdleFinishCandidate('detach');
|
|
2060
|
+
this.engine.clearIdleFinishCandidate('detach');
|
|
2249
2061
|
this.clearAllTimers();
|
|
2250
2062
|
this.pendingOutputParseChunks = [];
|
|
2251
2063
|
this.pendingTerminalQueryTail = '';
|
|
2252
2064
|
this.ptyOutputChunks = [];
|
|
2253
|
-
this.
|
|
2065
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2066
|
+
this.pendingOutboundQueue = [];
|
|
2067
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2254
2068
|
if (this.ptyProcess) {
|
|
2255
2069
|
try {
|
|
2256
2070
|
if (typeof this.ptyProcess.detach === 'function') {
|
|
@@ -2268,82 +2082,138 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2268
2082
|
}
|
|
2269
2083
|
|
|
2270
2084
|
clearHistory(): void {
|
|
2271
|
-
this.clearIdleFinishCandidate('clear_history');
|
|
2085
|
+
this.engine.clearIdleFinishCandidate('clear_history');
|
|
2272
2086
|
this.accumulatedBuffer = '';
|
|
2273
2087
|
this.accumulatedRawBuffer = '';
|
|
2274
|
-
this.currentTurnScope = null;
|
|
2275
|
-
this.submitRetryUsed = false;
|
|
2276
|
-
this.submitRetryPromptSnippet = '';
|
|
2088
|
+
this.engine.currentTurnScope = null;
|
|
2089
|
+
this.engine.submitRetryUsed = false;
|
|
2090
|
+
this.engine.submitRetryPromptSnippet = '';
|
|
2277
2091
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
2278
2092
|
this.pendingOutputParseChunks = [];
|
|
2279
2093
|
this.pendingTerminalQueryTail = '';
|
|
2280
2094
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
2281
2095
|
this.ptyOutputChunks = [];
|
|
2282
|
-
if (this.
|
|
2283
|
-
this.
|
|
2096
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2097
|
+
this.pendingOutboundQueue = [];
|
|
2098
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2284
2099
|
this.resetTerminalScreen();
|
|
2285
2100
|
this.ptyProcess?.clearBuffer?.();
|
|
2286
2101
|
this.onStatusChange?.();
|
|
2287
2102
|
}
|
|
2288
2103
|
|
|
2289
|
-
isProcessing(): boolean { return this.isWaitingForResponse; }
|
|
2104
|
+
isProcessing(): boolean { return this.engine.isWaitingForResponse; }
|
|
2290
2105
|
isReady(): boolean { return this.ready; }
|
|
2291
2106
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2107
|
+
// ─── State machine property accessors (delegate to engine) ──────────────
|
|
2108
|
+
// These expose engine state for external callers (tests, debug tools, etc.)
|
|
2109
|
+
|
|
2110
|
+
get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
|
|
2111
|
+
set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
|
|
2112
|
+
|
|
2113
|
+
get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
|
|
2114
|
+
set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
|
|
2115
|
+
|
|
2116
|
+
get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
|
|
2117
|
+
set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
|
|
2118
|
+
|
|
2119
|
+
get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
|
|
2120
|
+
set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
|
|
2121
|
+
// ARCH-REFACTOR R1: the mesh taskId bound to the most recently started turn,
|
|
2122
|
+
// surviving past turn settle until the next turn starts. The provider instance
|
|
2123
|
+
// reads this when stamping completion events so they carry the completing turn's
|
|
2124
|
+
// task rather than the racy last-write-wins session scalar.
|
|
2125
|
+
get currentTurnTaskId(): string | null { return this.engine.currentTurnTaskId; }
|
|
2126
|
+
// R4d: wall-clock when the most recently started turn began (persists past settle).
|
|
2127
|
+
// The provider instance's startup-grace idle-stayed synthesis anchors its window on
|
|
2128
|
+
// this so a delayed-dispatch first turn whose duration overruns the now-anchored
|
|
2129
|
+
// window is still attributed to the startup collapse.
|
|
2130
|
+
get currentTurnStartedAt(): number { return this.engine.currentTurnStartedAt; }
|
|
2131
|
+
|
|
2132
|
+
get responseEpoch(): number { return this.engine.responseEpoch; }
|
|
2133
|
+
set responseEpoch(v: number) { this.engine.responseEpoch = v; }
|
|
2134
|
+
|
|
2135
|
+
get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
|
|
2136
|
+
set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
|
|
2137
|
+
|
|
2138
|
+
get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
|
|
2139
|
+
set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
|
|
2140
|
+
|
|
2141
|
+
get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
|
|
2142
|
+
set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
|
|
2143
|
+
|
|
2144
|
+
get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
|
|
2145
|
+
set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
|
|
2146
|
+
|
|
2147
|
+
get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
|
|
2148
|
+
set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
|
|
2149
|
+
|
|
2150
|
+
get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
|
|
2151
|
+
get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
|
|
2152
|
+
|
|
2153
|
+
get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
|
|
2154
|
+
get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
|
|
2155
|
+
|
|
2156
|
+
get finishRetryCount(): number { return this.engine.finishRetryCount; }
|
|
2157
|
+
set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
|
|
2158
|
+
|
|
2159
|
+
get traceSessionId(): string { return this.engine.getTraceSessionId(); }
|
|
2160
|
+
get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
|
|
2161
|
+
get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
|
|
2162
|
+
get traceSeq(): number { return this.engine.getTraceEntries().length; }
|
|
2163
|
+
|
|
2164
|
+
/** Expose engine's evaluateSettled for test access */
|
|
2165
|
+
evaluateSettled(): void {
|
|
2166
|
+
LOG.debug(
|
|
2167
|
+
'CLI',
|
|
2168
|
+
`[${this.cliType}] settled diagnostics delegated to state engine`);
|
|
2169
|
+
this.engine.evaluateSettled(this.getSnapshot());
|
|
2170
|
+
}
|
|
2171
|
+
/** Expose engine's scheduleSettle for test access */
|
|
2172
|
+
scheduleSettle(): void { this.engine.scheduleSettle(); }
|
|
2173
|
+
/** Expose engine's clearIdleFinishCandidate for test access */
|
|
2174
|
+
clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
|
|
2175
|
+
/** Expose engine's finishResponse for test access */
|
|
2176
|
+
finishResponse(): void { this.engine.finishResponse(); }
|
|
2177
|
+
/** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
|
|
2178
|
+
getSnapshot(): CliBufferSnapshot {
|
|
2179
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
2180
|
+
return {
|
|
2181
|
+
accumulatedBuffer: this.accumulatedBuffer,
|
|
2182
|
+
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
2183
|
+
recentOutputBuffer: this.recentOutputBuffer,
|
|
2184
|
+
responseBuffer: this.responseBuffer,
|
|
2185
|
+
screenText,
|
|
2186
|
+
parseScreenText: this.getParseScreenText(screenText),
|
|
2187
|
+
workingDir: this.workingDir,
|
|
2188
|
+
providerSessionId: this.providerSessionId,
|
|
2189
|
+
runtimeSettings: this.runtimeSettings,
|
|
2190
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
2191
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2192
|
+
lastOutputAt: this.lastOutputAt,
|
|
2193
|
+
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2194
|
+
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2195
|
+
lastScreenSnapshot: this.lastScreenSnapshot,
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
isAlive(): boolean { return this.ptyProcess !== null; }
|
|
2199
|
+
flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
|
|
2200
|
+
|
|
2201
|
+
async writeRaw(data: string | Buffer): Promise<void> {
|
|
2202
|
+
const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
|
2203
|
+
await this.writeToPty(str);
|
|
2298
2204
|
}
|
|
2299
2205
|
|
|
2300
2206
|
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
|
-
}
|
|
2207
|
+
this.engine.resolveModal(buttonIndex);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined {
|
|
2211
|
+
return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/** Returns true if an approval was resolved within the adapter's cooldown window. */
|
|
2215
|
+
isApprovalRecentlyResolved(): boolean {
|
|
2216
|
+
return this.engine.isApprovalRecentlyResolved();
|
|
2347
2217
|
}
|
|
2348
2218
|
|
|
2349
2219
|
resize(cols: number, rows: number): void {
|
|
@@ -2357,7 +2227,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2357
2227
|
}
|
|
2358
2228
|
|
|
2359
2229
|
private getParsedDebugState(): Record<string, any> | null {
|
|
2360
|
-
if (this.startupParseGate ||
|
|
2230
|
+
if (this.startupParseGate || !this.runner.hasParseSession()) return null;
|
|
2361
2231
|
try {
|
|
2362
2232
|
const parsed = this.getScriptParsedStatus();
|
|
2363
2233
|
return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
|
|
@@ -2369,18 +2239,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2369
2239
|
getDebugState(): Record<string, any> {
|
|
2370
2240
|
const screenText = sanitizeTerminalText(this.terminalScreen.getText());
|
|
2371
2241
|
const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
2372
|
-
const
|
|
2373
|
-
|
|
2242
|
+
const startupDetectedStatus = this.startupParseGate && !startupModal
|
|
2243
|
+
? this.runDetectStatus(this.recentOutputBuffer || screenText)
|
|
2244
|
+
: null;
|
|
2245
|
+
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2374
2246
|
const parsedDebugState = this.getParsedDebugState();
|
|
2375
2247
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
2248
|
+
const hasFinalAssistant = (p: any) => {
|
|
2249
|
+
const msgs = Array.isArray(p?.messages) ? p.messages : [];
|
|
2250
|
+
return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
2251
|
+
};
|
|
2252
|
+
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
2253
|
+
if (parsedDebugState?.status === 'error') {
|
|
2254
|
+
effectiveStatus = 'error';
|
|
2255
|
+
}
|
|
2256
|
+
// (fix) Mirror the getStatus contract: never surface waiting_approval
|
|
2257
|
+
// without a concrete modal. detectStatus alone can fire while
|
|
2258
|
+
// parseApproval is still null, and a bare debugState waiting_approval
|
|
2259
|
+
// confused dashboards reading the debug payload.
|
|
2260
|
+
const debugEffectiveModal = startupModal || this.engine.activeModal;
|
|
2261
|
+
if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
|
|
2262
|
+
effectiveStatus = 'waiting_approval';
|
|
2263
|
+
}
|
|
2264
|
+
if (
|
|
2265
|
+
effectiveStatus === 'idle'
|
|
2266
|
+
&& parsedDebugState?.status === 'generating'
|
|
2267
|
+
&& !hasFinalAssistant(parsedDebugState)
|
|
2268
|
+
) {
|
|
2269
|
+
effectiveStatus = 'generating';
|
|
2270
|
+
}
|
|
2376
2271
|
return {
|
|
2377
2272
|
type: this.cliType,
|
|
2378
2273
|
name: this.cliName,
|
|
2379
2274
|
providerResolution: this.providerResolutionMeta,
|
|
2380
2275
|
status: effectiveStatus,
|
|
2381
2276
|
projectedStatus: effectiveStatus,
|
|
2382
|
-
rawStatus: this.currentStatus,
|
|
2383
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2277
|
+
rawStatus: this.engine.currentStatus,
|
|
2278
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2384
2279
|
ready: effectiveReady,
|
|
2385
2280
|
startupParseGate: this.startupParseGate,
|
|
2386
2281
|
spawnAt: this.spawnAt,
|
|
@@ -2394,38 +2289,47 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2394
2289
|
providerSessionId: parsedDebugState.providerSessionId,
|
|
2395
2290
|
transcriptAuthority: parsedDebugState.transcriptAuthority,
|
|
2396
2291
|
coverage: parsedDebugState.coverage,
|
|
2292
|
+
errorMessage: parsedDebugState.errorMessage,
|
|
2293
|
+
errorReason: parsedDebugState.errorReason,
|
|
2397
2294
|
activeModal: parsedDebugState.activeModal,
|
|
2398
2295
|
messageCount: parsedMessages.length,
|
|
2399
2296
|
} : null,
|
|
2400
2297
|
screenText: screenText.slice(-4000),
|
|
2401
|
-
currentTurnScope: this.currentTurnScope,
|
|
2298
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2402
2299
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
2403
2300
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
2404
|
-
settledBuffer: this.settledBuffer.slice(-500),
|
|
2405
2301
|
accumulatedBufferLength: this.accumulatedBuffer.length,
|
|
2406
2302
|
accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
|
|
2407
2303
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
2408
2304
|
sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
|
|
2409
2305
|
responseBuffer: this.responseBuffer.slice(-1000),
|
|
2306
|
+
pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
|
|
2307
|
+
id: message.id,
|
|
2308
|
+
role: message.role,
|
|
2309
|
+
content: message.content,
|
|
2310
|
+
queuedAt: message.queuedAt,
|
|
2311
|
+
source: message.source,
|
|
2312
|
+
})),
|
|
2313
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
2410
2314
|
lastOutputAt: this.lastOutputAt,
|
|
2411
2315
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2412
2316
|
lastScreenChangeAt: this.lastScreenChangeAt,
|
|
2413
2317
|
lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
|
|
2414
|
-
isWaitingForResponse: this.isWaitingForResponse,
|
|
2415
|
-
activeModal: startupModal || this.activeModal,
|
|
2416
|
-
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
2318
|
+
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
2319
|
+
activeModal: startupModal || this.engine.activeModal,
|
|
2320
|
+
lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
|
|
2417
2321
|
sendDelayMs: this.sendDelayMs,
|
|
2418
2322
|
sendKey: this.sendKey,
|
|
2419
2323
|
submitStrategy: this.submitStrategy,
|
|
2420
2324
|
requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
|
|
2421
|
-
submitPendingUntil: this.submitPendingUntil,
|
|
2422
|
-
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
2325
|
+
submitPendingUntil: this.engine.submitPendingUntil,
|
|
2326
|
+
responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
|
|
2423
2327
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
2424
2328
|
hasCliScripts: this.hasCliScripts(),
|
|
2425
|
-
scriptNames:
|
|
2426
|
-
traceSessionId: this.
|
|
2427
|
-
traceEntryCount: this.
|
|
2428
|
-
statusHistory: this.
|
|
2329
|
+
scriptNames: this.runner.getScriptNames(),
|
|
2330
|
+
traceSessionId: this.engine.getTraceSessionId(),
|
|
2331
|
+
traceEntryCount: this.engine.getTraceEntries().length,
|
|
2332
|
+
statusHistory: this.engine.getStatusHistory().slice(-30),
|
|
2429
2333
|
timeouts: this.timeouts,
|
|
2430
2334
|
pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
|
|
2431
2335
|
pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
|
|
@@ -2435,20 +2339,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2435
2339
|
|
|
2436
2340
|
getTraceState(limit = 120): Record<string, any> {
|
|
2437
2341
|
const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
|
|
2342
|
+
const traceEntries = this.engine.getTraceEntries();
|
|
2438
2343
|
return {
|
|
2439
|
-
sessionId: this.
|
|
2344
|
+
sessionId: this.engine.getTraceSessionId(),
|
|
2440
2345
|
providerResolution: this.providerResolutionMeta,
|
|
2441
|
-
entryCount:
|
|
2442
|
-
entries:
|
|
2346
|
+
entryCount: traceEntries.length,
|
|
2347
|
+
entries: traceEntries.slice(-cappedLimit),
|
|
2443
2348
|
screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
|
|
2444
2349
|
recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
|
|
2445
2350
|
responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
|
|
2446
2351
|
status: this.projectEffectiveStatus(),
|
|
2447
2352
|
projectedStatus: this.projectEffectiveStatus(),
|
|
2448
|
-
rawStatus: this.currentStatus,
|
|
2449
|
-
lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2450
|
-
activeModal: this.activeModal,
|
|
2451
|
-
currentTurnScope: this.currentTurnScope,
|
|
2353
|
+
rawStatus: this.engine.currentStatus,
|
|
2354
|
+
lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
|
|
2355
|
+
activeModal: this.engine.activeModal,
|
|
2356
|
+
currentTurnScope: this.engine.currentTurnScope,
|
|
2452
2357
|
messages: [],
|
|
2453
2358
|
};
|
|
2454
2359
|
}
|