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