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