@adhdev/daemon-core 0.9.82-rc.37 → 0.9.82-rc.370
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 +55 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +116 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +63 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +14 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/chat-commands.d.ts +27 -0
- package/dist/commands/cli-manager.d.ts +15 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +60 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +2 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +116 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +666 -2
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +77 -1
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +11 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +23 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/index.d.ts +2 -0
- package/dist/index.d.ts +55 -13
- package/dist/index.js +46516 -19739
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +43516 -16870
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +46 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +36 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +174 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +91 -0
- package/dist/mesh/mesh-events-pending.d.ts +55 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +61 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-init.d.ts +86 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +77 -1
- package/dist/mesh/mesh-missions.d.ts +164 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +429 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +246 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +216 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +20 -0
- package/dist/providers/chat-message-normalization.d.ts +12 -0
- package/dist/providers/cli-provider-instance.d.ts +108 -3
- package/dist/providers/contracts.d.ts +139 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +63 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +27 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +14 -0
- package/dist/providers/provider-instance.d.ts +23 -1
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +91 -0
- package/dist/providers/spec/cli-adapter.d.ts +223 -0
- package/dist/providers/spec/evaluator.d.ts +45 -0
- package/dist/providers/spec/fsm-driver.d.ts +395 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +76 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +174 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +222 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +295 -13
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +59 -2
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +26 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +63 -12
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +56 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1221 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +775 -1141
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +110 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +90 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
- package/src/commands/chat-commands.ts +1917 -62
- package/src/commands/cli-manager.ts +502 -24
- package/src/commands/handler.ts +841 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +592 -0
- package/src/commands/high-family/mesh-events.ts +47 -0
- package/src/commands/high-family/mesh-status.ts +642 -0
- package/src/commands/high-family/types.ts +76 -0
- package/src/commands/low-family/coordinator-prompt.ts +72 -0
- package/src/commands/low-family/daemon-lifecycle.ts +107 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +81 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +274 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/med-family/cli-agent.ts +218 -0
- package/src/commands/med-family/fast-forward.ts +198 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +35 -0
- package/src/commands/med-family/mesh-crud.ts +788 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +131 -0
- package/src/commands/med-family/types.ts +120 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router.ts +5121 -2369
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +483 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +389 -6
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +28 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +55 -14
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +574 -43
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +8 -1
- package/src/git/index.ts +16 -0
- package/src/index.ts +158 -12
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/command-log.ts +7 -5
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +220 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +338 -0
- package/src/mesh/coordinator-prompt.ts +234 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +645 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +2866 -0
- package/src/mesh/mesh-events-pending.ts +599 -0
- package/src/mesh/mesh-events-stale.ts +309 -0
- package/src/mesh/mesh-events-utils.ts +387 -0
- package/src/mesh/mesh-events.ts +30 -1035
- package/src/mesh/mesh-fast-forward.ts +839 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-init.ts +260 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +591 -102
- package/src/mesh/mesh-missions.ts +322 -0
- package/src/mesh/mesh-reconcile-loop.ts +1134 -0
- package/src/mesh/mesh-refine-batch.ts +197 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +1697 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +997 -141
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +423 -0
- package/src/mesh/worktree-bootstrap-config.ts +331 -0
- package/src/providers/acp-provider-instance.ts +43 -10
- package/src/providers/approval-utils.d.ts +5 -0
- package/src/providers/approval-utils.ts +57 -5
- package/src/providers/chat-message-normalization.ts +45 -5
- package/src/providers/cli-provider-instance.ts +1310 -101
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +150 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +85 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/dispatcher.ts +340 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +19 -1
- package/src/providers/provider-loader.ts +668 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +235 -0
- package/src/providers/spec/cli-adapter.ts +1051 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1305 -0
- package/src/providers/spec/fsm-evaluator.ts +272 -0
- package/src/providers/spec/fsm-loader.ts +120 -0
- package/src/providers/spec/fsm-types.ts +273 -0
- package/src/providers/spec/native-history-executor.ts +1136 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +262 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +378 -14
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +64 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +95 -26
- package/src/system/hash.ts +23 -0
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- package/src/types.ts +5 -0
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -13,19 +13,25 @@ import { createRequire } from 'node:module';
|
|
|
13
13
|
import { normalizeInputEnvelope, type ProviderModule, flattenContent, type InputEnvelope, type InputPart } from './contracts.js';
|
|
14
14
|
import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
|
|
15
15
|
import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
|
|
16
|
+
import { normalizeInteractivePrompt, normalizeInteractivePromptResponse, type InteractivePrompt } from './types/interactive-prompt.js';
|
|
16
17
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
18
|
+
import { shortHash } from '../system/hash.js';
|
|
17
19
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
20
|
+
import { createCliAdapter } from './spec/route.js';
|
|
18
21
|
import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
19
22
|
import { StatusMonitor } from './status-monitor.js';
|
|
20
23
|
import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderNativeHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
|
|
21
24
|
import { LOG } from '../logging/logger.js';
|
|
25
|
+
import { traceMeshEventStage, traceMeshEventDrop } from '../mesh/mesh-event-trace.js';
|
|
22
26
|
import type { ChatMessage } from '../types.js';
|
|
23
27
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
24
|
-
import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
|
|
28
|
+
import { formatAutoApprovalMessage, pickApprovalButton, hasNegativeApprovalOption, looksLikeActiveApprovalPromptText } from './approval-utils.js';
|
|
25
29
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
26
30
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
27
31
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
28
32
|
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
|
|
33
|
+
import { workingDirBasename } from './working-dir.js';
|
|
34
|
+
import { ManualAttendanceTracker } from './manual-attendance.js';
|
|
29
35
|
|
|
30
36
|
type PersistableCliHistoryMessage = {
|
|
31
37
|
role: string;
|
|
@@ -35,17 +41,77 @@ type PersistableCliHistoryMessage = {
|
|
|
35
41
|
receivedAt?: number;
|
|
36
42
|
};
|
|
37
43
|
|
|
44
|
+
// Status snapshots only ever surface the newest messages: the cloud 'live'
|
|
45
|
+
// profile drops chat messages entirely (loaded lazily via read_chat on
|
|
46
|
+
// subscribe) and the 'full' profile caps activeChat.messages to the last 60
|
|
47
|
+
// (see status/normalize.ts). Unread/completion markers walk only the tail.
|
|
48
|
+
// So getState()'s saved-history hydration — which runs once per resume/manual
|
|
49
|
+
// CLI session on every status report — must read only a bounded tail, not the
|
|
50
|
+
// entire transcript. A full MAX_SAFE_INTEGER read here makes the initial
|
|
51
|
+
// status report O(transcript) × N(sessions), which is the real cold first-
|
|
52
|
+
// connection bottleneck on chat-heavy machines. The window comfortably exceeds
|
|
53
|
+
// the 60-message snapshot cap so dedup/collapse at the boundary stays stable.
|
|
54
|
+
const STATUS_HYDRATION_TAIL_LIMIT = 200;
|
|
55
|
+
|
|
38
56
|
type CompletedDebouncePending = {
|
|
39
57
|
chatTitle: string;
|
|
40
58
|
duration: number;
|
|
41
59
|
timestamp: number;
|
|
42
60
|
firstObservedAt: number;
|
|
61
|
+
previousStatus: string;
|
|
43
62
|
loggedBlockReason?: string;
|
|
63
|
+
loggedTranscriptProbe?: boolean;
|
|
64
|
+
transcriptProbeHistory?: ExternalTranscriptProbe[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function isIdleStatus(value: unknown): boolean {
|
|
68
|
+
const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
69
|
+
return !status || status === 'idle' || status === 'ready';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
function getMessageTime(message: unknown): number {
|
|
74
|
+
if (!message || typeof message !== 'object') return 0;
|
|
75
|
+
const record = message as { receivedAt?: unknown; timestamp?: unknown };
|
|
76
|
+
const value = Number(record.receivedAt ?? record.timestamp ?? 0);
|
|
77
|
+
return Number.isFinite(value) ? value : 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type CompletedFinalizationBlock = {
|
|
81
|
+
reason: string;
|
|
82
|
+
terminal?: boolean;
|
|
83
|
+
allowTimeout?: boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type CompletionFinalAssistantEvidence = {
|
|
87
|
+
present: boolean;
|
|
88
|
+
messages: unknown[];
|
|
89
|
+
source: 'parsed' | 'external-native' | 'unavailable';
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type ExternalTranscriptProbe = {
|
|
93
|
+
readAt: number;
|
|
94
|
+
msgCount: number;
|
|
95
|
+
lastRole: string | null;
|
|
96
|
+
lastKind: string | null;
|
|
97
|
+
contentLen: number;
|
|
98
|
+
sourcePath: string | null;
|
|
99
|
+
sourceMtimeMs: number | null;
|
|
100
|
+
mtimeAgeMs: number | null;
|
|
44
101
|
};
|
|
45
102
|
|
|
46
103
|
const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
47
104
|
const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
48
105
|
|
|
106
|
+
/** Events that signal a dispatched mesh task has reached a terminal state.
|
|
107
|
+
* Detach the mesh assignment after emitting one of these so the worker's
|
|
108
|
+
* next unrelated turn doesn't impersonate another completion. */
|
|
109
|
+
const TERMINAL_MESH_EVENTS = new Set([
|
|
110
|
+
'agent:generating_completed',
|
|
111
|
+
'agent:stopped',
|
|
112
|
+
'agent:ready',
|
|
113
|
+
]);
|
|
114
|
+
|
|
49
115
|
const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
|
|
50
116
|
'image/png': '.png',
|
|
51
117
|
'image/jpeg': '.jpg',
|
|
@@ -120,7 +186,7 @@ function hasNonEmptyCliModalButtons(activeModal: unknown): boolean {
|
|
|
120
186
|
}
|
|
121
187
|
|
|
122
188
|
function isCliGeneratingLikeStatus(status: unknown): boolean {
|
|
123
|
-
return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
|
|
189
|
+
return status === 'generating' || status === 'streaming' || status === 'no_progress' || status === 'long_generating' || status === 'starting';
|
|
124
190
|
}
|
|
125
191
|
|
|
126
192
|
export function buildCliStructuredInputPrompt(
|
|
@@ -300,6 +366,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
300
366
|
readonly type: string;
|
|
301
367
|
readonly category = 'cli' as const;
|
|
302
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Quiet period an approval modal's signature must be stable before
|
|
371
|
+
* auto-approve sends the approve key. Guards against firing on a prompt
|
|
372
|
+
* that is still streaming into the PTY (the "resolves too fast" symptom):
|
|
373
|
+
* while the modal text/buttons are still changing, every frame yields a
|
|
374
|
+
* new signature and the settle clock restarts. Once the prompt finishes
|
|
375
|
+
* rendering the signature holds and the key is sent after this window.
|
|
376
|
+
* Bounded + small so genuine approvals stay timely. The FSM is already
|
|
377
|
+
* authoritative over the `waiting_approval` state; this only delays the
|
|
378
|
+
* keystroke until the modal *content* has settled.
|
|
379
|
+
*/
|
|
380
|
+
private static readonly AUTO_APPROVE_SETTLE_MS = 600;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Busy-side hysteresis for the settle gate. A momentary `generating` flip
|
|
384
|
+
* while the SAME approval modal's button block is still on screen (its
|
|
385
|
+
* question line scrolled out of the captured frame, only the buttons + a
|
|
386
|
+
* residual `esc to interrupt` spinner remain) briefly reports
|
|
387
|
+
* status!=waiting_approval. Without hysteresis that flip wipes the settle
|
|
388
|
+
* clock, and the modal→generating→modal flap restarts the 600ms window
|
|
389
|
+
* every time so auto-approve never fires. We keep the in-progress settle
|
|
390
|
+
* gate warm across an inactive blip up to this bound; only once the modal
|
|
391
|
+
* has genuinely stayed gone this long (a real resolution → idle) is the
|
|
392
|
+
* gate cleared. Bounded so a genuinely new, later approval still re-settles
|
|
393
|
+
* from scratch rather than firing on a stale timestamp.
|
|
394
|
+
*/
|
|
395
|
+
private static readonly AUTO_APPROVE_GATE_HYSTERESIS_MS = 1500;
|
|
396
|
+
|
|
303
397
|
private adapter: ProviderCliAdapter;
|
|
304
398
|
private context: InstanceContext | null = null;
|
|
305
399
|
private events: ProviderEvent[] = [];
|
|
@@ -309,15 +403,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
309
403
|
private monitor: StatusMonitor;
|
|
310
404
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
311
405
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
312
|
-
private
|
|
406
|
+
private lastApprovalEventFingerprint = '';
|
|
313
407
|
private autoApproveBusy = false;
|
|
314
408
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
409
|
+
private lastAutoApprovalSignature = '';
|
|
410
|
+
// Settle gate: the approval modal's signature + the wall-clock when this
|
|
411
|
+
// exact signature was first observed. Auto-approve only fires once the
|
|
412
|
+
// SAME signature has been stable for AUTO_APPROVE_SETTLE_MS, so a prompt
|
|
413
|
+
// still streaming into the PTY (its buttons/message changing frame to
|
|
414
|
+
// frame) keeps resetting the timer and is never approved half-rendered.
|
|
415
|
+
private pendingAutoApprovalSignature = '';
|
|
416
|
+
private pendingAutoApprovalSince = 0;
|
|
417
|
+
private autoApproveSettleTimer: NodeJS.Timeout | null = null;
|
|
418
|
+
// Wall-clock when auto-approve first observed status!=waiting_approval while
|
|
419
|
+
// a settle gate was in progress. Drives AUTO_APPROVE_GATE_HYSTERESIS_MS so a
|
|
420
|
+
// brief generating flip does not immediately wipe the settle clock.
|
|
421
|
+
private autoApproveInactiveSince = 0;
|
|
422
|
+
// Provider-common manual-attendance signal: while a human is actively driving
|
|
423
|
+
// this session from the dashboard, auto-approve holds so they can take manual
|
|
424
|
+
// control. Background mesh workers are never attended → delegated auto-approve
|
|
425
|
+
// is unaffected.
|
|
426
|
+
private readonly manualAttendance = new ManualAttendanceTracker();
|
|
315
427
|
private controlValues: Record<string, string | number | boolean> = {};
|
|
316
428
|
private summaryMetadata: unknown = undefined;
|
|
317
429
|
private appliedEffectKeys = new Set<string>();
|
|
318
430
|
private historyWriter: ChatHistoryWriter;
|
|
319
431
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
320
432
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
433
|
+
private lastAcknowledgedUserInputAt = 0;
|
|
321
434
|
private lastNativeSourceCanonicalCheckAt = 0;
|
|
322
435
|
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
323
436
|
private cachedSqliteDb: {
|
|
@@ -330,6 +443,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
330
443
|
private suppressIdleHistoryReplay = false;
|
|
331
444
|
private errorMessage: string | undefined = undefined;
|
|
332
445
|
private errorReason: ProviderErrorReason | undefined = undefined;
|
|
446
|
+
private activeInteractivePrompt: InteractivePrompt | null = null;
|
|
333
447
|
|
|
334
448
|
private presentationMode: 'terminal' | 'chat';
|
|
335
449
|
private providerSessionId?: string;
|
|
@@ -370,7 +484,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
370
484
|
this.providerSessionId = options?.providerSessionId;
|
|
371
485
|
this.launchMode = options?.launchMode || 'new';
|
|
372
486
|
this.onProviderSessionResolved = options?.onProviderSessionResolved;
|
|
373
|
-
this.adapter =
|
|
487
|
+
this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
|
|
488
|
+
if (this.providerSessionId) {
|
|
489
|
+
this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
|
|
490
|
+
}
|
|
374
491
|
this.monitor = new StatusMonitor();
|
|
375
492
|
this.historyWriter = new ChatHistoryWriter();
|
|
376
493
|
}
|
|
@@ -389,8 +506,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
389
506
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
390
507
|
this.monitor.updateConfig({
|
|
391
508
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
392
|
-
|
|
393
|
-
|
|
509
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
510
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
394
511
|
});
|
|
395
512
|
|
|
396
513
|
// Server connection
|
|
@@ -412,7 +529,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
412
529
|
await this.adapter.spawn();
|
|
413
530
|
await this.enforceFreshSessionLaunchIfNeeded();
|
|
414
531
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
415
|
-
if (this.providerSessionId) {
|
|
532
|
+
if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
|
|
416
533
|
this.restorePersistedHistoryFromCurrentSession();
|
|
417
534
|
}
|
|
418
535
|
if (this.providerSessionId && this.launchMode === 'resume') {
|
|
@@ -486,14 +603,38 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
486
603
|
}
|
|
487
604
|
|
|
488
605
|
getState(): ProviderState {
|
|
606
|
+
// TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
|
|
607
|
+
// parseSession) are currently invoked by CliScriptRunner.invoke() via direct
|
|
608
|
+
// function calls — the scripts run in the daemon process with full Node.js
|
|
609
|
+
// access and no resource limits.
|
|
610
|
+
//
|
|
611
|
+
// When Phase 5 lands, CliScriptRunner should route these calls through a
|
|
612
|
+
// SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
|
|
613
|
+
// that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
|
|
614
|
+
// a 32 MB memory cap. The execution path to change is:
|
|
615
|
+
// CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
|
|
616
|
+
//
|
|
617
|
+
// This getState() call-site is NOT where the change goes — the wiring belongs
|
|
618
|
+
// in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
|
|
619
|
+
// parseSession), with provider-loader.ts updated to store script source strings
|
|
620
|
+
// alongside the loaded function references for extended-legacy providers.
|
|
489
621
|
const adapterStatus = this.adapter.getStatus();
|
|
622
|
+
if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
|
|
623
|
+
this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
624
|
+
}
|
|
490
625
|
let parsedStatus: any = null;
|
|
491
626
|
let parseErrorMessage: string | undefined;
|
|
492
627
|
if (typeof this.adapter.getScriptParsedStatus === 'function') {
|
|
493
628
|
try {
|
|
494
629
|
parsedStatus = this.adapter.getScriptParsedStatus() || null;
|
|
495
|
-
|
|
496
|
-
|
|
630
|
+
const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
|
|
631
|
+
? parsedStatus.errorMessage.trim()
|
|
632
|
+
: undefined;
|
|
633
|
+
const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
|
|
634
|
+
? parsedStatus.errorReason.trim() as ProviderErrorReason
|
|
635
|
+
: undefined;
|
|
636
|
+
this.errorMessage = parsedErrorMessage;
|
|
637
|
+
this.errorReason = parsedErrorReason;
|
|
497
638
|
} catch (error: any) {
|
|
498
639
|
parseErrorMessage = error?.message || String(error);
|
|
499
640
|
this.errorMessage = parseErrorMessage;
|
|
@@ -503,22 +644,53 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
503
644
|
this.errorMessage = undefined;
|
|
504
645
|
this.errorReason = undefined;
|
|
505
646
|
}
|
|
506
|
-
const
|
|
507
|
-
const visibleStatus = parseErrorMessage
|
|
508
|
-
? 'error'
|
|
509
|
-
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
510
|
-
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
647
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
511
648
|
this.provider,
|
|
512
|
-
typeof
|
|
649
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
513
650
|
);
|
|
514
|
-
|
|
515
|
-
|
|
651
|
+
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
652
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
653
|
+
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
654
|
+
? 'error'
|
|
655
|
+
: (autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status);
|
|
656
|
+
// getState() must agree with the status the FSM-driven detectStatusTransition()
|
|
657
|
+
// already committed to lastStatus. The adapter's own status is authoritative; we do
|
|
658
|
+
// not second-guess it with native-transcript shape. Only reconcile a generating-like
|
|
659
|
+
// read down to idle when our own lastStatus has already flipped idle (avoids a
|
|
660
|
+
// perpetual dashboard spinner during the brief window before the next getStatus()).
|
|
661
|
+
if (isCliGeneratingLikeStatus(visibleStatus) && this.lastStatus === 'idle') {
|
|
662
|
+
visibleStatus = 'idle';
|
|
516
663
|
}
|
|
517
664
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
518
665
|
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
519
666
|
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
520
667
|
? parsedStatus.messages
|
|
521
668
|
: [];
|
|
669
|
+
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
670
|
+
this.provider,
|
|
671
|
+
typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
|
|
672
|
+
);
|
|
673
|
+
const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
|
|
674
|
+
parsedMessages,
|
|
675
|
+
parsedStatus,
|
|
676
|
+
adapterStatus,
|
|
677
|
+
parsedProviderSessionId,
|
|
678
|
+
);
|
|
679
|
+
if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
680
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
681
|
+
}
|
|
682
|
+
if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
683
|
+
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
684
|
+
}
|
|
685
|
+
if (suppressFreshLaunchStartupReplay) {
|
|
686
|
+
parsedMessages = [];
|
|
687
|
+
}
|
|
688
|
+
// Adapter runtime metadata is transport-owned and is not guaranteed to
|
|
689
|
+
// identify this conversation. Spec adapters historically exposed the
|
|
690
|
+
// provider spec id (for example "codex-cli") as runtimeId, which made
|
|
691
|
+
// concurrent sessions share one activeChat identity until their native
|
|
692
|
+
// provider session ids were discovered.
|
|
693
|
+
const activeChatId = this.providerSessionId || this.instanceId;
|
|
522
694
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
523
695
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
524
696
|
: null;
|
|
@@ -528,9 +700,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
528
700
|
: [];
|
|
529
701
|
}
|
|
530
702
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
531
|
-
const canonicalBackedHistory = this.
|
|
703
|
+
const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
|
|
704
|
+
? this.syncCanonicalSavedHistoryIfNeeded()
|
|
705
|
+
: false;
|
|
706
|
+
const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
|
|
707
|
+
? this.lastPersistedHistoryMessages.map((message) => ({
|
|
708
|
+
role: message.role,
|
|
709
|
+
content: message.content,
|
|
710
|
+
kind: message.kind,
|
|
711
|
+
senderName: message.senderName,
|
|
712
|
+
receivedAt: message.receivedAt,
|
|
713
|
+
}))
|
|
714
|
+
: mergedMessages;
|
|
532
715
|
|
|
533
|
-
const dirName = this.workingDir
|
|
716
|
+
const dirName = workingDirBasename(this.workingDir);
|
|
534
717
|
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
535
718
|
? parsedStatus.status.trim()
|
|
536
719
|
: undefined;
|
|
@@ -542,7 +725,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
542
725
|
&& adapterStatus.status === 'idle'
|
|
543
726
|
&& parsedStatus?.status === 'idle';
|
|
544
727
|
let messagesToSave = parsedMessages;
|
|
545
|
-
if (!suppressStaleParsedBusyStatus && (parsedChatStatus === 'generating' || parsedChatStatus === 'long_generating')) {
|
|
728
|
+
if (!suppressStaleParsedBusyStatus && (parsedChatStatus === 'generating' || parsedChatStatus === 'no_progress' || parsedChatStatus === 'long_generating')) {
|
|
546
729
|
const lastIdx = messagesToSave.length - 1;
|
|
547
730
|
if (lastIdx >= 0 && messagesToSave[lastIdx]?.role === 'assistant') {
|
|
548
731
|
messagesToSave = messagesToSave.slice(0, lastIdx);
|
|
@@ -572,33 +755,48 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
572
755
|
}
|
|
573
756
|
}
|
|
574
757
|
|
|
575
|
-
this.applyProviderResponse(
|
|
758
|
+
this.applyProviderResponse(
|
|
759
|
+
suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
|
|
760
|
+
? { ...parsedStatus, providerSessionId: undefined }
|
|
761
|
+
: parsedStatus,
|
|
762
|
+
{ phase: 'immediate' },
|
|
763
|
+
);
|
|
576
764
|
const surface = resolveProviderStateSurface({
|
|
577
765
|
summaryMetadata: this.summaryMetadata as any,
|
|
578
766
|
controlValues: this.controlValues,
|
|
579
767
|
});
|
|
580
768
|
const activeChatStatus = parseErrorMessage
|
|
581
769
|
? 'error'
|
|
582
|
-
: autoApproveActive && parsedStatus?.status === 'waiting_approval'
|
|
770
|
+
: (autoApproveActive && parsedStatus?.status === 'waiting_approval') || autoApproveHoldIdle
|
|
583
771
|
? 'generating'
|
|
584
772
|
: (adapterStatus.status !== 'idle'
|
|
585
773
|
? visibleStatus
|
|
586
774
|
: (suppressStaleParsedBusyStatus ? visibleStatus : (parsedChatStatus || visibleStatus)));
|
|
587
775
|
|
|
776
|
+
// If an AskUserQuestion prompt is awaiting user input, overlay status as
|
|
777
|
+
// waiting_choice. This is distinct from waiting_approval (tool-use consent)
|
|
778
|
+
// — the engine's isWaitingForResponse state is unchanged, so completion
|
|
779
|
+
// tracking continues normally once the user responds.
|
|
780
|
+
const hasInteractivePrompt = !!this.activeInteractivePrompt;
|
|
781
|
+
const finalStatus = hasInteractivePrompt ? 'waiting_choice' : visibleStatus;
|
|
782
|
+
const finalChatStatus = hasInteractivePrompt ? 'waiting_choice' : activeChatStatus;
|
|
783
|
+
|
|
588
784
|
return {
|
|
589
785
|
type: this.type,
|
|
590
786
|
name: this.provider.name,
|
|
591
787
|
category: 'cli',
|
|
592
|
-
status:
|
|
788
|
+
status: finalStatus,
|
|
593
789
|
mode: this.presentationMode,
|
|
594
790
|
activeChat: {
|
|
595
|
-
id:
|
|
791
|
+
id: activeChatId,
|
|
596
792
|
title: parsedStatus?.title || dirName,
|
|
597
|
-
status:
|
|
598
|
-
messages:
|
|
599
|
-
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
793
|
+
status: finalChatStatus,
|
|
794
|
+
messages: statusMessages,
|
|
795
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
796
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
600
797
|
inputContent: '',
|
|
601
798
|
},
|
|
799
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
602
800
|
workspace: this.workingDir,
|
|
603
801
|
instanceId: this.instanceId,
|
|
604
802
|
providerSessionId: this.providerSessionId,
|
|
@@ -638,8 +836,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
638
836
|
|
|
639
837
|
getHotChatSessionState(): HotChatSessionState {
|
|
640
838
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
641
|
-
const autoApproveActive = adapterStatus.status
|
|
642
|
-
const
|
|
839
|
+
const autoApproveActive = this.autoApproveEffectivelyActive(adapterStatus.status);
|
|
840
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
841
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
643
842
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
644
843
|
return {
|
|
645
844
|
id: this.instanceId,
|
|
@@ -651,36 +850,165 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
651
850
|
};
|
|
652
851
|
}
|
|
653
852
|
|
|
654
|
-
getSessionModalState(): SessionModalState {
|
|
655
|
-
const adapterStatus = this.adapter.getStatus({ allowParse:
|
|
656
|
-
const autoApproveActive = adapterStatus.status
|
|
657
|
-
const
|
|
658
|
-
const
|
|
853
|
+
getSessionModalState(sessionId?: string): SessionModalState {
|
|
854
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
855
|
+
const autoApproveActive = this.autoApproveEffectivelyActive(adapterStatus.status);
|
|
856
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
857
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
858
|
+
const dirName = workingDirBasename(this.workingDir);
|
|
659
859
|
return {
|
|
660
|
-
|
|
860
|
+
// Honor the caller-supplied sessionId — InstanceMgr rejects the
|
|
861
|
+
// projection when projected.id !== requested sessionId, and
|
|
862
|
+
// this.instanceId is the manager's internal key, not the public
|
|
863
|
+
// sessionId the dashboard subscribes by.
|
|
864
|
+
id: sessionId ?? this.instanceId,
|
|
661
865
|
status: visibleStatus,
|
|
662
866
|
title: dirName,
|
|
663
|
-
activeModal: autoApproveActive ? null : adapterStatus.activeModal,
|
|
867
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : adapterStatus.activeModal,
|
|
664
868
|
};
|
|
665
869
|
}
|
|
666
870
|
|
|
667
871
|
updateSettings(newSettings: Record<string, any>): void {
|
|
668
|
-
|
|
872
|
+
// Merge semantics: a key omitted from newSettings preserves its existing
|
|
873
|
+
// value, a key present in newSettings (even as false) overrides it.
|
|
874
|
+
//
|
|
875
|
+
// This is required because updateSettings has two callers with opposite
|
|
876
|
+
// intent:
|
|
877
|
+
// 1. Full re-injection — the dashboard toggle path (handleSetProviderSetting
|
|
878
|
+
// → getSettings → updateInstanceSettings) sends the COMPLETE settings
|
|
879
|
+
// object, so an explicit autoApprove:false must win.
|
|
880
|
+
// 2. Partial stamp — the mesh relay-safety stamp (router.ts agent_command,
|
|
881
|
+
// buildMeshWorkerRelayStamp) sends ONLY {meshNodeFor, meshNodeId,
|
|
882
|
+
// meshCoordinatorDaemonId, launchedByCoordinator} on every coordinator
|
|
883
|
+
// re-dispatch. It carries no autoApprove, so a full replacement would
|
|
884
|
+
// wipe the launch-time autoApprove:true the worker was started with,
|
|
885
|
+
// silently dropping every later approval to a manual gate until the
|
|
886
|
+
// machine-page toggle re-injected the full settings.
|
|
887
|
+
//
|
|
888
|
+
// A plain merge satisfies both: undefined keys fall through to the existing
|
|
889
|
+
// value (preserving launch-stamp settings like autoApprove + the mesh routing
|
|
890
|
+
// keys), explicit keys override. This subsumes the previous mesh-key preserve
|
|
891
|
+
// list, which only protected the routing keys and not autoApprove.
|
|
892
|
+
this.settings = { ...this.settings, ...newSettings };
|
|
669
893
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
670
894
|
this.monitor.updateConfig({
|
|
671
895
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
672
|
-
|
|
673
|
-
|
|
896
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
897
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
674
898
|
});
|
|
675
899
|
}
|
|
676
900
|
|
|
901
|
+
/**
|
|
902
|
+
* Stamp a direct-dispatch mesh assignment on this instance.
|
|
903
|
+
* setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
|
|
904
|
+
* route generating_completed back to the originating coordinator. Without
|
|
905
|
+
* this stamp, mesh_send_task --direct targets a plain CLI session whose
|
|
906
|
+
* completion events silently drop because the forwarder has nothing to
|
|
907
|
+
* match against.
|
|
908
|
+
*/
|
|
909
|
+
attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string; coordinatorDaemonId?: string; coordinatorSessionId?: string }): void {
|
|
910
|
+
if (!assignment?.meshId) return;
|
|
911
|
+
this.settings = {
|
|
912
|
+
...this.settings,
|
|
913
|
+
meshNodeFor: assignment.meshId,
|
|
914
|
+
// WTCLAIM (A): track the bound node id under BOTH the active marker
|
|
915
|
+
// (meshNodeId, cleared on detach) and a sticky marker (meshLastNodeId,
|
|
916
|
+
// preserved across detach). The sticky marker lets a detached but still
|
|
917
|
+
// coordinator-owned session be re-picked ONLY for the SAME node it served
|
|
918
|
+
// — never auto-adopted for a sibling node (e.g. a cloned worktree) that
|
|
919
|
+
// shares this daemon. See isMeshOwnedDelegateSession's post-detach gate.
|
|
920
|
+
...(assignment.nodeId ? { meshNodeId: assignment.nodeId, meshLastNodeId: assignment.nodeId } : {}),
|
|
921
|
+
...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
|
|
922
|
+
...(assignment.coordinatorDaemonId ? { meshCoordinatorDaemonId: assignment.coordinatorDaemonId } : {}),
|
|
923
|
+
// Session-level routing anchor: the originating coordinator session, so this
|
|
924
|
+
// worker's completion events route back to the exact session that dispatched it.
|
|
925
|
+
...(assignment.coordinatorSessionId ? { meshCoordinatorSessionId: assignment.coordinatorSessionId } : {}),
|
|
926
|
+
};
|
|
927
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Clear a previously-attached mesh assignment after the task reaches a
|
|
932
|
+
* terminal state. Leaving meshNodeFor pinned would route this session's
|
|
933
|
+
* subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
|
|
934
|
+
* coordinator as if they were task completions.
|
|
935
|
+
*/
|
|
936
|
+
detachMeshAssignment(): void {
|
|
937
|
+
if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
|
|
938
|
+
const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
|
|
939
|
+
void meshNodeFor; void meshActiveTaskId;
|
|
940
|
+
// WTCLAIM (A): clear the active binding but PRESERVE the last bound node id
|
|
941
|
+
// (meshLastNodeId) so a later sessionless dispatch can re-adopt this idle
|
|
942
|
+
// session ONLY for the node it last served. Carry the id being cleared, or
|
|
943
|
+
// keep an already-present sticky marker if meshNodeId was absent.
|
|
944
|
+
const lastNodeId = (typeof meshNodeId === 'string' && meshNodeId.trim())
|
|
945
|
+
? meshNodeId.trim()
|
|
946
|
+
: (typeof rest.meshLastNodeId === 'string' && rest.meshLastNodeId.trim() ? rest.meshLastNodeId.trim() : undefined);
|
|
947
|
+
this.settings = lastNodeId ? { ...rest, meshLastNodeId: lastNodeId } : rest;
|
|
948
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* The resolved modal-park status of this session, or null when it is not
|
|
953
|
+
* parked on a modal awaiting a human answer. Mirrors the overlay logic in
|
|
954
|
+
* getState(): an active AskUserQuestion interactive prompt resolves to
|
|
955
|
+
* waiting_choice; otherwise the adapter's waiting_approval (tool consent)
|
|
956
|
+
* counts — UNLESS auto-approve will dismiss it, in which case the session is
|
|
957
|
+
* effectively generating and is NOT modal-parked. This is the single signal
|
|
958
|
+
* the mesh force-inject guard consults, and the same status string the
|
|
959
|
+
* reconcile loop reads off get_status_metadata. Lowercase literals only —
|
|
960
|
+
* the SessionStatus enum is forked across modules and waiting_choice is
|
|
961
|
+
* absent from some of them.
|
|
962
|
+
*/
|
|
963
|
+
resolveModalParkStatus(): 'waiting_choice' | 'waiting_approval' | null {
|
|
964
|
+
if (this.activeInteractivePrompt) return 'waiting_choice';
|
|
965
|
+
let adapterStatus: { status?: string };
|
|
966
|
+
try {
|
|
967
|
+
adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
968
|
+
} catch {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
// A session whose auto-approve is held by manual attendance IS parked on a
|
|
972
|
+
// modal awaiting the human — autoApproveEffectivelyActive folds that in, so
|
|
973
|
+
// the mesh force-inject guard correctly treats it as modal-parked.
|
|
974
|
+
if (adapterStatus.status === 'waiting_approval' && !this.autoApproveEffectivelyActive(adapterStatus.status)) {
|
|
975
|
+
return 'waiting_approval';
|
|
976
|
+
}
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** True when this session is parked on a modal awaiting a human answer. */
|
|
981
|
+
isModalParked(): boolean {
|
|
982
|
+
return this.resolveModalParkStatus() !== null;
|
|
983
|
+
}
|
|
984
|
+
|
|
677
985
|
onEvent(event: string, data?: any): void {
|
|
678
986
|
if (event === 'send_message') {
|
|
679
987
|
const input = normalizeInputEnvelope(data);
|
|
680
988
|
assertProviderSupportsDeclaredInput(this.provider, input);
|
|
681
989
|
const promptText = buildCliStructuredInputPrompt(input);
|
|
682
990
|
if (promptText) {
|
|
683
|
-
|
|
991
|
+
// force:true bypasses the busy/generating send guard so terminal mesh
|
|
992
|
+
// events (completion/failure/bootstrap) land in a coordinator session that
|
|
993
|
+
// is itself parked in `generating` while awaiting that very event.
|
|
994
|
+
// Without it the message is queued and only flushed on the coordinator's
|
|
995
|
+
// own idle transition — which never happens until it receives the message.
|
|
996
|
+
const force = data?.force === true;
|
|
997
|
+
// Modal guard: a force-inject still writes raw keystrokes into the PTY,
|
|
998
|
+
// bypassing the busy send-guard. If the coordinator is parked on a
|
|
999
|
+
// harness modal (claude-cli AskUserQuestion → waiting_choice, or a
|
|
1000
|
+
// tool-consent waiting_approval), those keystrokes are consumed by the
|
|
1001
|
+
// modal's key handler and silently select a choice the user never made
|
|
1002
|
+
// (data corruption). Hold the force-inject in that narrow window —
|
|
1003
|
+
// the event stays queued and the reconcile loop redelivers it on the
|
|
1004
|
+
// next tick once the modal is resolved. We ONLY hold for the two modal
|
|
1005
|
+
// states; generating is still force-injected (that is the deadlock the
|
|
1006
|
+
// force path exists to break — see mesh-events-coordinator).
|
|
1007
|
+
if (force && this.isModalParked()) {
|
|
1008
|
+
LOG.info('CLI', `[${this.type}] force send_message held — coordinator parked on modal (${this.resolveModalParkStatus()})`);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
void this.adapter.sendMessage(promptText, force ? { force: true } : {}).catch((e: any) => {
|
|
684
1012
|
LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
|
|
685
1013
|
});
|
|
686
1014
|
}
|
|
@@ -690,14 +1018,69 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
690
1018
|
void this.adapter.resolveAction(data).catch((e: any) => {
|
|
691
1019
|
LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
|
|
692
1020
|
});
|
|
1021
|
+
} else if (event === 'interactive_prompt' && data) {
|
|
1022
|
+
const prompt = normalizeInteractivePrompt(data);
|
|
1023
|
+
if (prompt) {
|
|
1024
|
+
this.activeInteractivePrompt = prompt;
|
|
1025
|
+
this.events.push({
|
|
1026
|
+
event: 'interactive_prompt',
|
|
1027
|
+
timestamp: Date.now(),
|
|
1028
|
+
promptId: prompt.promptId,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
} else if (event === 'interactive_prompt_response' && data) {
|
|
1032
|
+
try {
|
|
1033
|
+
const response = normalizeInteractivePromptResponse(data);
|
|
1034
|
+
if (this.activeInteractivePrompt?.promptId === response.promptId) {
|
|
1035
|
+
this.activeInteractivePrompt = null;
|
|
1036
|
+
}
|
|
1037
|
+
if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
|
|
1038
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
|
|
1042
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
|
|
1043
|
+
});
|
|
1044
|
+
} catch (e: any) {
|
|
1045
|
+
LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
|
|
1046
|
+
}
|
|
693
1047
|
} else if (event === 'provider_state_patch' && data && typeof data === 'object') {
|
|
694
1048
|
this.applyProviderResponse(data, { phase: 'immediate' });
|
|
695
1049
|
}
|
|
696
1050
|
}
|
|
697
1051
|
|
|
1052
|
+
recordAcknowledgedUserInput(input: InputEnvelope | string): void {
|
|
1053
|
+
const content = typeof input === 'string'
|
|
1054
|
+
? input.trim()
|
|
1055
|
+
: buildCliStructuredInputPrompt(input).trim();
|
|
1056
|
+
if (!content) return;
|
|
1057
|
+
|
|
1058
|
+
const receivedAt = Date.now();
|
|
1059
|
+
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
1060
|
+
const dedupKey = `user_input_ack:${shortHash(`${this.instanceId}:${content}:${receivedAt}`, 24)}`;
|
|
1061
|
+
this.appendRuntimeMessage(buildChatMessage({
|
|
1062
|
+
role: 'user',
|
|
1063
|
+
senderName: 'User',
|
|
1064
|
+
kind: 'standard',
|
|
1065
|
+
content,
|
|
1066
|
+
receivedAt,
|
|
1067
|
+
timestamp: receivedAt,
|
|
1068
|
+
source: 'runtime_input_ack',
|
|
1069
|
+
meta: {
|
|
1070
|
+
runtimeInputAck: true,
|
|
1071
|
+
provider: this.type,
|
|
1072
|
+
workspace: this.workingDir,
|
|
1073
|
+
},
|
|
1074
|
+
} as ChatMessage), dedupKey);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
698
1077
|
dispose(): void {
|
|
699
1078
|
this.adapter.shutdown();
|
|
700
1079
|
this.monitor.reset();
|
|
1080
|
+
// Cancel any armed auto-approve timers so a pending settle re-check
|
|
1081
|
+
// can't fire resolveModal/detectStatusTransition against a dead adapter.
|
|
1082
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
1083
|
+
if (this.autoApproveBusyTimer) { clearTimeout(this.autoApproveBusyTimer); this.autoApproveBusyTimer = null; }
|
|
701
1084
|
this.appliedEffectKeys.clear();
|
|
702
1085
|
try { this.cachedSqliteDb?.close(); } catch { /* noop */ }
|
|
703
1086
|
this.cachedSqliteDb = null;
|
|
@@ -706,6 +1089,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
706
1089
|
|
|
707
1090
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
708
1091
|
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
1092
|
+
private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
|
|
709
1093
|
|
|
710
1094
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
711
1095
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -740,7 +1124,180 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
740
1124
|
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
741
1125
|
const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
|
|
742
1126
|
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
743
|
-
|
|
1127
|
+
if (role !== 'assistant' || !content) return false;
|
|
1128
|
+
// Guard: if the last assistant message looks like an active approval/input prompt,
|
|
1129
|
+
// it is not a real completion — the session is still awaiting user input.
|
|
1130
|
+
if (looksLikeActiveApprovalPromptText(content)) return false;
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
|
|
1135
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1136
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1137
|
+
const readAt = Date.now();
|
|
1138
|
+
const mtimeMs = Number(sourceMtimeMs) || 0;
|
|
1139
|
+
return {
|
|
1140
|
+
readAt,
|
|
1141
|
+
msgCount: messages.length,
|
|
1142
|
+
lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
|
|
1143
|
+
lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
|
|
1144
|
+
contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
|
|
1145
|
+
sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
|
|
1146
|
+
sourceMtimeMs: mtimeMs || null,
|
|
1147
|
+
mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
|
|
1152
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1153
|
+
if (!probe) return null;
|
|
1154
|
+
const history = pending.transcriptProbeHistory || [];
|
|
1155
|
+
const last = history[history.length - 1];
|
|
1156
|
+
if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
|
|
1157
|
+
history.push(probe);
|
|
1158
|
+
pending.transcriptProbeHistory = history.slice(-5);
|
|
1159
|
+
}
|
|
1160
|
+
return probe;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private readExternalCompletionMessages(): unknown[] | null {
|
|
1164
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1165
|
+
if (!adapterOwnsMessagesElsewhere) return null;
|
|
1166
|
+
if (!this.providerSessionId) return null;
|
|
1167
|
+
if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
|
|
1168
|
+
|
|
1169
|
+
if (this.lastExternalCompletionProbe?.sourcePath) {
|
|
1170
|
+
try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
1171
|
+
}
|
|
1172
|
+
const restoredHistory = readProviderChatHistory(this.type, {
|
|
1173
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1174
|
+
historySessionId: this.providerSessionId,
|
|
1175
|
+
workspace: this.workingDir,
|
|
1176
|
+
offset: 0,
|
|
1177
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1178
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1179
|
+
scripts: this.provider.scripts as any,
|
|
1180
|
+
sessionStartedAtMs: this.startedAt,
|
|
1181
|
+
forceRefresh: true,
|
|
1182
|
+
});
|
|
1183
|
+
if (restoredHistory.source !== 'provider-native') {
|
|
1184
|
+
this.lastExternalCompletionProbe = null;
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
|
|
1188
|
+
restoredHistory.messages,
|
|
1189
|
+
restoredHistory.sourcePath,
|
|
1190
|
+
restoredHistory.sourceMtimeMs,
|
|
1191
|
+
);
|
|
1192
|
+
return restoredHistory.messages;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
|
|
1196
|
+
if (this.completionHasFinalAssistantMessage(parsedMessages)) {
|
|
1197
|
+
return {
|
|
1198
|
+
present: true,
|
|
1199
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1200
|
+
source: 'parsed',
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1205
|
+
if (externalMessages) {
|
|
1206
|
+
return {
|
|
1207
|
+
present: this.completionHasFinalAssistantMessage(externalMessages),
|
|
1208
|
+
messages: externalMessages,
|
|
1209
|
+
source: 'external-native',
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return {
|
|
1214
|
+
present: false,
|
|
1215
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1216
|
+
source: 'unavailable',
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private completionFinalSummary(parsedMessages: unknown): string | undefined {
|
|
1221
|
+
// For native-source providers (claude-cli: chatMessagesOwnedExternally), the PTY
|
|
1222
|
+
// screen parse is NOT the source of truth for the final summary — the terminal
|
|
1223
|
+
// wraps/scrolls/clips text, so a screen-parsed assistant message is often a partial
|
|
1224
|
+
// prefix (e.g. 76 chars of a 112-char turn). The append-only native transcript holds
|
|
1225
|
+
// the complete turn. Prefer it whenever it yields a longer/complete summary; fall back
|
|
1226
|
+
// to the parsed screen only when the transcript is unavailable. This is the real cause
|
|
1227
|
+
// of the truncated finalSummary — independent of cloud vs standalone (it surfaces on
|
|
1228
|
+
// any short, fast-completing task where screen parse wins the race).
|
|
1229
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1230
|
+
const parsedSummary = extractFinalSummaryFromMessages(
|
|
1231
|
+
(this.completionHasFinalAssistantMessage(parsedMessages)
|
|
1232
|
+
? (Array.isArray(parsedMessages) ? parsedMessages : [])
|
|
1233
|
+
: []) as any,
|
|
1234
|
+
);
|
|
1235
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1236
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1237
|
+
const externalSummary = externalMessages
|
|
1238
|
+
? extractFinalSummaryFromMessages(externalMessages as any)
|
|
1239
|
+
: '';
|
|
1240
|
+
// The transcript is authoritative for native-source providers. Use it unless it is
|
|
1241
|
+
// empty (not yet written) — only then fall back to whatever the screen parsed.
|
|
1242
|
+
if (externalSummary) return externalSummary;
|
|
1243
|
+
return parsedSummary || undefined;
|
|
1244
|
+
}
|
|
1245
|
+
return parsedSummary || undefined;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
1249
|
+
blockReason: string;
|
|
1250
|
+
latestStatus?: any;
|
|
1251
|
+
latestVisibleStatus: string;
|
|
1252
|
+
waitedMs: number;
|
|
1253
|
+
pending: CompletedDebouncePending;
|
|
1254
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
1255
|
+
}): Record<string, unknown> {
|
|
1256
|
+
let parsed: any = null;
|
|
1257
|
+
let parseError: string | undefined;
|
|
1258
|
+
try {
|
|
1259
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
1260
|
+
} catch (error: any) {
|
|
1261
|
+
parseError = error?.message || String(error);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1265
|
+
if (evidence.source === 'external-native') {
|
|
1266
|
+
this.recordPendingTranscriptProbe(args.pending);
|
|
1267
|
+
}
|
|
1268
|
+
const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
|
|
1269
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1270
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1271
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
1272
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
1273
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
1274
|
+
|
|
1275
|
+
return {
|
|
1276
|
+
providerType: this.type,
|
|
1277
|
+
sessionId: this.instanceId,
|
|
1278
|
+
providerSessionId: this.providerSessionId || null,
|
|
1279
|
+
workspace: this.workingDir,
|
|
1280
|
+
blockReason: args.blockReason,
|
|
1281
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
1282
|
+
waitedMs: args.waitedMs,
|
|
1283
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
1284
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
1285
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
1286
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
1287
|
+
parseError: parseError || undefined,
|
|
1288
|
+
finalAssistantPresent: evidence.present,
|
|
1289
|
+
finalAssistantEvidenceSource: evidence.source,
|
|
1290
|
+
visibleMessageCount: visibleMessages.length,
|
|
1291
|
+
lastVisibleRole,
|
|
1292
|
+
lastVisibleKind,
|
|
1293
|
+
lastVisibleContentLength,
|
|
1294
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
1295
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
1296
|
+
pendingTimestamp: args.pending.timestamp,
|
|
1297
|
+
pendingDurationSec: args.pending.duration,
|
|
1298
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
1299
|
+
transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
|
|
1300
|
+
};
|
|
744
1301
|
}
|
|
745
1302
|
|
|
746
1303
|
private hasAdapterPendingResponse(): boolean {
|
|
@@ -765,41 +1322,172 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
765
1322
|
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
766
1323
|
if (adapterRawStatus !== 'idle') return false;
|
|
767
1324
|
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
768
|
-
|
|
1325
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
1326
|
+
// Do not suppress when the adapter's raw response buffer is still non-empty.
|
|
1327
|
+
// This catches the case where isWaitingForResponse has already flipped to false
|
|
1328
|
+
// (so getPartialResponse() returns '') but the provider's native parser still
|
|
1329
|
+
// reports generating because it's parsing buffered content. Suppressing the
|
|
1330
|
+
// finalization block here would emit a false completion event while the provider
|
|
1331
|
+
// session is still actively processing its response stream.
|
|
1332
|
+
const adapterAny = this.adapter as any;
|
|
1333
|
+
if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
|
|
1334
|
+
return true;
|
|
769
1335
|
}
|
|
770
1336
|
|
|
771
|
-
private
|
|
772
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
1337
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1338
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
773
1339
|
|
|
774
1340
|
const adapterAny = this.adapter as any;
|
|
775
|
-
|
|
776
|
-
if (
|
|
1341
|
+
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1342
|
+
if (!approvalResolvedIdle) {
|
|
1343
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1344
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1345
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
1346
|
+
}
|
|
777
1347
|
|
|
778
1348
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
1349
|
? this.adapter.getPartialResponse()
|
|
780
1350
|
: '';
|
|
781
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
1351
|
+
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
782
1352
|
|
|
783
1353
|
let parsed: any;
|
|
784
1354
|
try {
|
|
785
1355
|
parsed = this.adapter.getScriptParsedStatus();
|
|
786
1356
|
} catch (error: any) {
|
|
787
|
-
return `parse_error:${error?.message || String(error)}
|
|
1357
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
788
1358
|
}
|
|
789
1359
|
|
|
790
1360
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
-
if (parsedStatus !== 'idle')
|
|
792
|
-
|
|
793
|
-
|
|
1361
|
+
if (parsedStatus !== 'idle') {
|
|
1362
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1363
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1364
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1365
|
+
}
|
|
1366
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1367
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1368
|
+
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1369
|
+
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1370
|
+
LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
|
|
1371
|
+
if (!finalAssistantEvidence.present) {
|
|
1372
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1373
|
+
if (finalAssistantEvidence.source === 'external-native') {
|
|
1374
|
+
const probe = this.recordPendingTranscriptProbe(pending);
|
|
1375
|
+
if (probe && !pending.loggedTranscriptProbe) {
|
|
1376
|
+
LOG.info('CLI', `[${this.type}] external transcript probe: msgCount=${probe.msgCount} lastRole=${probe.lastRole || 'none'} lastKind=${probe.lastKind || 'none'} contentLen=${probe.contentLen} sourceMtime=${probe.sourceMtimeMs ?? 'unknown'} mtimeAge=${probe.mtimeAgeMs ?? 'unknown'}ms`);
|
|
1377
|
+
pending.loggedTranscriptProbe = true;
|
|
1378
|
+
}
|
|
1379
|
+
LOG.debug('CLI', `[${this.type}] external-native probe result: lastRole=${probe?.lastRole} contentLen=${probe?.contentLen}`);
|
|
1380
|
+
if (probe?.lastRole === 'assistant' && (probe.contentLen ?? 0) > 0) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
if (this.type === 'antigravity-cli') {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1387
|
+
}
|
|
1388
|
+
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1389
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1390
|
+
}
|
|
1391
|
+
} else {
|
|
1392
|
+
LOG.debug('CLI', `[${this.type}] missing_final_assistant (not ownsExternal) requiresFinalAssistant=${!!(this.provider as any).requiresFinalAssistantBeforeIdle}`);
|
|
1393
|
+
return {
|
|
1394
|
+
reason: 'missing_final_assistant',
|
|
1395
|
+
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1396
|
+
allowTimeout: allowMissingAssistantTimeout,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// (FALSEIDLE-a) Structural approval-resolution gate. Runs BEFORE the brittle
|
|
1402
|
+
// screen-text heuristic below so it also catches modals whose text does not match
|
|
1403
|
+
// looksLikeActiveApprovalPromptText (e.g. claude-cli's cd / "untrusted hooks" prompt).
|
|
1404
|
+
const approvalResolutionBlock = this.approvalResolutionFinalizationBlock(pending);
|
|
1405
|
+
if (approvalResolutionBlock) return approvalResolutionBlock;
|
|
1406
|
+
|
|
1407
|
+
// Guard: if the screen still shows an approval/choice prompt as the last visible text,
|
|
1408
|
+
// the turn is not complete even if the parsed status says idle and there is an assistant
|
|
1409
|
+
// message. This catches the case where waiting_approval→idle transitions occur before
|
|
1410
|
+
// the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
|
|
1411
|
+
try {
|
|
1412
|
+
const screenText = typeof (this.adapter as any).getScreenText === 'function'
|
|
1413
|
+
? String((this.adapter as any).getScreenText() || '')
|
|
1414
|
+
: '';
|
|
1415
|
+
if (screenText) {
|
|
1416
|
+
const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
|
|
1417
|
+
if (looksLikeActiveApprovalPromptText(tailLines)) {
|
|
1418
|
+
return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
} catch { /* defensive: screen text read is best-effort */ }
|
|
794
1422
|
|
|
795
1423
|
return null;
|
|
796
1424
|
}
|
|
797
1425
|
|
|
1426
|
+
// (FALSEIDLE-a) Positive, structural proof that the latest approval entry was resolved
|
|
1427
|
+
// through ADHDev. resolveModal() — driven by auto-approve, dashboard/mesh_approve, and
|
|
1428
|
+
// dev-cli-debug alike — advances the engine's lastResolvedEntrySeq to the current
|
|
1429
|
+
// approvalEntrySeq. So `lastResolvedEntrySeq >= approvalEntrySeq` (with a real entry,
|
|
1430
|
+
// approvalEntrySeq > 0) means the modal we last saw was actually answered. Absence of this
|
|
1431
|
+
// evidence after a waiting_approval→idle transition means the idle is suspect: the spec's
|
|
1432
|
+
// text-based approval→idle rule false-tripped while the modal is still unresolved.
|
|
1433
|
+
// Fails OPEN (returns true) when the seq fields are unavailable, so the gate can never wedge
|
|
1434
|
+
// a session on a provider/adapter that does not surface the counters.
|
|
1435
|
+
private hasApprovalResolutionEvidence(): boolean {
|
|
1436
|
+
try {
|
|
1437
|
+
const status = this.adapter.getStatus({ allowParse: false }) as any;
|
|
1438
|
+
const entrySeq = typeof status?.approvalEntrySeq === 'number' ? status.approvalEntrySeq : 0;
|
|
1439
|
+
if (entrySeq <= 0) return true;
|
|
1440
|
+
const resolvedSeq = typeof status?.lastResolvedEntrySeq === 'number' ? status.lastResolvedEntrySeq : undefined;
|
|
1441
|
+
if (resolvedSeq === undefined) return true;
|
|
1442
|
+
return resolvedSeq >= entrySeq;
|
|
1443
|
+
} catch {
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// (FALSEIDLE-a) Hold a completion that is the anomalous DIRECT waiting_approval→idle
|
|
1449
|
+
// transition with no positive resolution evidence. A genuinely resolved approval routes
|
|
1450
|
+
// through resolveModal → setStatus('generating'), so its completion's previousStatus is
|
|
1451
|
+
// 'generating' (not 'waiting_approval') and this gate never fires for it. Scoped to
|
|
1452
|
+
// delegated mesh/coordinator sessions — whose only modal-resolution path is auto-approve /
|
|
1453
|
+
// mesh_approve (both advance lastResolvedEntrySeq) — so an interactive local session, where
|
|
1454
|
+
// a human may answer the PTY prompt directly and leave no resolveModal record, is untouched.
|
|
1455
|
+
// Non-terminal: the hold is bounded by COMPLETED_FINALIZATION_MAX_WAIT_MS (30s), giving a
|
|
1456
|
+
// settling auto-approve time to fire and advance the seq, and guaranteeing no permanent wedge
|
|
1457
|
+
// if resolution ever happens via a path that does not record evidence.
|
|
1458
|
+
private approvalResolutionFinalizationBlock(pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1459
|
+
if (pending.previousStatus !== 'waiting_approval') return null;
|
|
1460
|
+
const meshContext = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1461
|
+
if (!meshContext) return null;
|
|
1462
|
+
if (this.hasApprovalResolutionEvidence()) return null;
|
|
1463
|
+
return { reason: 'approval_resolution_unconfirmed', terminal: false };
|
|
1464
|
+
}
|
|
1465
|
+
|
|
798
1466
|
private scheduleCompletedDebounceFlush(delayMs: number): void {
|
|
799
1467
|
if (this.completedDebounceTimer) clearTimeout(this.completedDebounceTimer);
|
|
800
1468
|
this.completedDebounceTimer = setTimeout(() => this.flushCompletedDebounceIfFinalized(), delayMs);
|
|
801
1469
|
}
|
|
802
1470
|
|
|
1471
|
+
// EVTTRACE (observation-only): is this a mesh worker session whose completion
|
|
1472
|
+
// events must route to a coordinator? Used purely to gate trace logging so a
|
|
1473
|
+
// non-mesh CLI session's completions don't add EvtTrace noise. No decision logic.
|
|
1474
|
+
private isMeshWorkerSession(): boolean {
|
|
1475
|
+
return !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId
|
|
1476
|
+
|| this.settings.meshNodeId || this.settings.launchedByCoordinator);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// EVTTRACE correlation context for this session's completion lifecycle. taskId is
|
|
1480
|
+
// the primary grep anchor; instanceId is the session fallback.
|
|
1481
|
+
private meshTraceCtx(event = 'agent:generating_completed'): Record<string, unknown> {
|
|
1482
|
+
return {
|
|
1483
|
+
taskId: this.settings.meshActiveTaskId,
|
|
1484
|
+
sessionId: this.instanceId,
|
|
1485
|
+
nodeId: this.settings.meshNodeId,
|
|
1486
|
+
meshId: this.settings.meshNodeFor,
|
|
1487
|
+
event,
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
|
|
803
1491
|
private flushCompletedDebounceIfFinalized(): void {
|
|
804
1492
|
const pending = this.completedDebouncePending;
|
|
805
1493
|
if (!pending) {
|
|
@@ -809,7 +1497,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
809
1497
|
|
|
810
1498
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
811
1499
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
812
|
-
const latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
|
|
1500
|
+
const latestVisibleStatus = latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status;
|
|
1501
|
+
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
813
1502
|
if (latestVisibleStatus !== 'idle') {
|
|
814
1503
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
815
1504
|
this.completedDebouncePending = null;
|
|
@@ -817,84 +1506,350 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
1506
|
return;
|
|
818
1507
|
}
|
|
819
1508
|
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
1509
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1510
|
+
if (block) {
|
|
1511
|
+
const blockReason = block.reason;
|
|
822
1512
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
-
|
|
1513
|
+
// CANON-C (completion-gate decouple): a block carrying `allowTimeout` is the
|
|
1514
|
+
// transcript-evidence gate — the worker FSM has ALREADY reached idle and the only
|
|
1515
|
+
// thing missing is the append-only transcript's final assistant turn (a native-source
|
|
1516
|
+
// race: claude-cli owns its history externally and the file write trails the idle
|
|
1517
|
+
// transition). `allowTimeout` is set ONLY on the missing_final_assistant block, and
|
|
1518
|
+
// ONLY for mesh worker sessions (meshNodeFor / meshActiveTaskId / launchedByCoordinator).
|
|
1519
|
+
// The coordinator's sole path to learn this session is idle is agent:generating_completed,
|
|
1520
|
+
// so holding it up to COMPLETED_FINALIZATION_MAX_WAIT_MS (30s) leaves the coordinator
|
|
1521
|
+
// false-generating while the worker is done. Decouple the idle NOTIFICATION from the
|
|
1522
|
+
// transcript evidence: emit the completion immediately, marked weak
|
|
1523
|
+
// (completionDiagnostic.blockReason=missing_final_assistant, finalAssistantPresent=false).
|
|
1524
|
+
// The finalSummary is enriched on a SEPARATE path — the mesh reconcile loop reads the
|
|
1525
|
+
// transcript once written and re-emits a GENUINE completion (CANON-B weak→genuine
|
|
1526
|
+
// upgrade; buildPendingEventFingerprint keeps weak and genuine distinct so the enriched
|
|
1527
|
+
// one still surfaces, and isFalseIdleCompletion keeps the direct dispatch active until
|
|
1528
|
+
// then). All OTHER blocks (genuinely-busy adapter/partial/parsed states, transient
|
|
1529
|
+
// parse_error) keep the existing terminal-hold / 30s-retry behavior unchanged.
|
|
1530
|
+
const isTranscriptEvidenceGate = block.allowTimeout === true;
|
|
1531
|
+
LOG.debug('CLI', `[${this.type}] finalization block: reason=${blockReason} terminal=${block.terminal} allowTimeout=${isTranscriptEvidenceGate} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
1532
|
+
if (!isTranscriptEvidenceGate && (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS)) {
|
|
824
1533
|
if (pending.loggedBlockReason !== blockReason) {
|
|
825
1534
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
1535
|
+
// EVTTRACE: completion held by the finalization gate (CANON-C). Observation
|
|
1536
|
+
// only — does not change the hold decision above.
|
|
1537
|
+
if (this.isMeshWorkerSession()) {
|
|
1538
|
+
traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `${blockReason} waited=${waitedMs}ms`);
|
|
1539
|
+
}
|
|
826
1540
|
pending.loggedBlockReason = blockReason;
|
|
827
1541
|
}
|
|
828
1542
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
1543
|
return;
|
|
830
1544
|
}
|
|
831
|
-
|
|
1545
|
+
const emittedAfterFinalizationTimeout = waitedMs >= COMPLETED_FINALIZATION_MAX_WAIT_MS;
|
|
1546
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
1547
|
+
blockReason,
|
|
1548
|
+
latestStatus,
|
|
1549
|
+
latestVisibleStatus,
|
|
1550
|
+
waitedMs,
|
|
1551
|
+
pending,
|
|
1552
|
+
emittedAfterFinalizationTimeout,
|
|
1553
|
+
});
|
|
1554
|
+
// Surface the CANON-C immediate-emit path distinctly so a delegated worker's idle
|
|
1555
|
+
// notification (transcript still pending) is not mistaken for a 30s-timeout fallback.
|
|
1556
|
+
(completionDiagnostic as Record<string, unknown>).decoupledImmediateEmit = isTranscriptEvidenceGate && !emittedAfterFinalizationTimeout;
|
|
1557
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event (${isTranscriptEvidenceGate && !emittedAfterFinalizationTimeout ? 'CANON-C decoupled-immediate, transcript pending' : `after ${waitedMs}ms`}) without finalized assistant turn (${blockReason})`);
|
|
1558
|
+
// EVTTRACE: completion fired (forced past the finalization timeout / CANON-C decoupled-immediate).
|
|
1559
|
+
if (this.isMeshWorkerSession()) {
|
|
1560
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), `forced after ${waitedMs}ms (${blockReason})`);
|
|
1561
|
+
}
|
|
1562
|
+
this.pushEvent({
|
|
1563
|
+
event: 'agent:generating_completed',
|
|
1564
|
+
chatTitle: pending.chatTitle,
|
|
1565
|
+
duration: pending.duration,
|
|
1566
|
+
timestamp: pending.timestamp,
|
|
1567
|
+
// When finalization is forced past the timeout on a `parsed_status:` block
|
|
1568
|
+
// (the parser never confirmed a final assistant turn) we previously rode an
|
|
1569
|
+
// empty `finalSummary` unconditionally. That empty value propagates to the
|
|
1570
|
+
// mesh coordinator's mirror preview (meshSessionLastMessagePreview), leaving a
|
|
1571
|
+
// delegated session's inbox preview blank — or, for a LOCAL worktree session,
|
|
1572
|
+
// stuck on the dispatched user task. If the parser DID surface assistant text,
|
|
1573
|
+
// prefer it; only fall back to '' when no assistant summary can be derived.
|
|
1574
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
1575
|
+
? (this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages) ?? '')
|
|
1576
|
+
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1577
|
+
completionDiagnostic,
|
|
1578
|
+
});
|
|
832
1579
|
this.completedDebouncePending = null;
|
|
833
1580
|
this.completedDebounceTimer = null;
|
|
834
1581
|
this.generatingStartedAt = 0;
|
|
1582
|
+
this.lastApprovalEventFingerprint = '';
|
|
835
1583
|
return;
|
|
836
1584
|
}
|
|
837
1585
|
|
|
838
1586
|
LOG.info('CLI', `[${this.type}] completed in ${pending.duration}s`);
|
|
1587
|
+
// EVTTRACE: completion fired (transcript finalized cleanly).
|
|
1588
|
+
if (this.isMeshWorkerSession()) {
|
|
1589
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), `duration=${pending.duration}s`);
|
|
1590
|
+
}
|
|
839
1591
|
this.pushEvent({
|
|
840
1592
|
event: 'agent:generating_completed',
|
|
841
1593
|
chatTitle: pending.chatTitle,
|
|
842
1594
|
duration: pending.duration,
|
|
843
1595
|
timestamp: pending.timestamp,
|
|
844
|
-
finalSummary:
|
|
1596
|
+
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
845
1597
|
});
|
|
846
1598
|
this.completedDebouncePending = null;
|
|
847
1599
|
this.completedDebounceTimer = null;
|
|
848
1600
|
this.generatingStartedAt = 0;
|
|
1601
|
+
this.lastApprovalEventFingerprint = '';
|
|
849
1602
|
}
|
|
850
1603
|
|
|
851
1604
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
1605
|
+
// Manual-attendance suppression (provider-common): when a human is
|
|
1606
|
+
// actively driving this session from the dashboard, hold auto-approve so
|
|
1607
|
+
// the modal stays visible and they can pick a button / use the controlbar
|
|
1608
|
+
// themselves. Return false (NOT auto-approving) so getState keeps the
|
|
1609
|
+
// modal surfaced. Clear any in-progress settle gate — a genuine fire
|
|
1610
|
+
// after the window lapses must re-settle from scratch — and arm a
|
|
1611
|
+
// re-check for the lapse moment, because the PTY may have gone silent and
|
|
1612
|
+
// would otherwise never re-drive this decision. Background mesh workers
|
|
1613
|
+
// are never attended, so their delegated auto-approve is untouched.
|
|
1614
|
+
if (adapterStatus?.status === 'waiting_approval'
|
|
1615
|
+
&& this.shouldAutoApprove()
|
|
1616
|
+
&& this.manualAttendance.isAttended(now)) {
|
|
1617
|
+
this.lastAutoApprovalSignature = '';
|
|
1618
|
+
this.pendingAutoApprovalSignature = '';
|
|
1619
|
+
this.pendingAutoApprovalSince = 0;
|
|
1620
|
+
this.autoApproveInactiveSince = 0;
|
|
1621
|
+
if (this.autoApproveSettleTimer) clearTimeout(this.autoApproveSettleTimer);
|
|
1622
|
+
this.autoApproveSettleTimer = setTimeout(() => {
|
|
1623
|
+
this.autoApproveSettleTimer = null;
|
|
1624
|
+
this.recheckAutoApproveSettled();
|
|
1625
|
+
}, this.manualAttendance.remainingMs(now) + 20);
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
852
1628
|
const autoApproveActive = adapterStatus?.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
853
1629
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
854
1630
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
855
1631
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1632
|
+
// However, Claude Code can present a second approval immediately after the
|
|
1633
|
+
// first. Resolve a changed modal signature even while the previous write is
|
|
1634
|
+
// still inside the short busy window.
|
|
1635
|
+
if (!autoApproveActive) {
|
|
1636
|
+
this.lastAutoApprovalSignature = '';
|
|
1637
|
+
// Hysteresis: if a settle gate is mid-progress, a momentary
|
|
1638
|
+
// status!=waiting_approval blip (a generating flip while the same
|
|
1639
|
+
// modal's button block is still on screen) must NOT wipe the settle
|
|
1640
|
+
// clock — otherwise the modal→generating→modal flap restarts the
|
|
1641
|
+
// 600ms window every time and auto-approve never fires. Keep the
|
|
1642
|
+
// gate warm for AUTO_APPROVE_GATE_HYSTERESIS_MS; re-arm a timer so
|
|
1643
|
+
// that if the modal does NOT come back the gate is cleared on the
|
|
1644
|
+
// re-check (a genuine resolution → idle frees the gate normally).
|
|
1645
|
+
if (this.pendingAutoApprovalSince) {
|
|
1646
|
+
if (!this.autoApproveInactiveSince) this.autoApproveInactiveSince = now;
|
|
1647
|
+
const goneForMs = now - this.autoApproveInactiveSince;
|
|
1648
|
+
if (goneForMs < CliProviderInstance.AUTO_APPROVE_GATE_HYSTERESIS_MS) {
|
|
1649
|
+
if (this.autoApproveSettleTimer) clearTimeout(this.autoApproveSettleTimer);
|
|
1650
|
+
this.autoApproveSettleTimer = setTimeout(() => {
|
|
1651
|
+
this.autoApproveSettleTimer = null;
|
|
1652
|
+
this.recheckAutoApproveSettled();
|
|
1653
|
+
}, CliProviderInstance.AUTO_APPROVE_GATE_HYSTERESIS_MS - goneForMs + 20);
|
|
1654
|
+
return autoApproveActive;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
// Clear the settle gate so the next approval starts its own quiet
|
|
1658
|
+
// window from scratch (a stale timestamp would let it fire instantly).
|
|
1659
|
+
this.pendingAutoApprovalSignature = '';
|
|
1660
|
+
this.pendingAutoApprovalSince = 0;
|
|
1661
|
+
this.autoApproveInactiveSince = 0;
|
|
1662
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
1663
|
+
return autoApproveActive;
|
|
1664
|
+
}
|
|
1665
|
+
// Active approval observed — reset the inactivity tracker so a later
|
|
1666
|
+
// blip starts its hysteresis window fresh.
|
|
1667
|
+
this.autoApproveInactiveSince = 0;
|
|
1668
|
+
const modal = adapterStatus.activeModal;
|
|
1669
|
+
// (fix) Do not auto-approve when no concrete modal/buttons are present.
|
|
1670
|
+
// Claude TUI flaps between paints; without this guard adapterStatus
|
|
1671
|
+
// could report status=waiting_approval with activeModal=null (or with
|
|
1672
|
+
// an empty buttons array briefly) and we'd still call
|
|
1673
|
+
// resolveModal(-1) — which used to type "1" into the prompt
|
|
1674
|
+
// repeatedly. Skip until a real modal is captured.
|
|
1675
|
+
const buttons = Array.isArray(modal?.buttons)
|
|
1676
|
+
? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
|
|
1677
|
+
: [];
|
|
1678
|
+
if (!modal || buttons.length === 0) {
|
|
1679
|
+
return autoApproveActive;
|
|
1680
|
+
}
|
|
1681
|
+
// Picker/confirm exclusion (provider-common). A /model or /mode picker is
|
|
1682
|
+
// surfaced with status=waiting_approval so the dashboard shows it, but it
|
|
1683
|
+
// has no "correct" answer to auto-pick — blindly selecting the first
|
|
1684
|
+
// option silently switches the model (the "always Opus, before I even
|
|
1685
|
+
// choose" bug). Two independent gates, BOTH must pass to fire:
|
|
1686
|
+
//
|
|
1687
|
+
// (1) modal_kind — the spec/FSM tells us this is an 'approval' modal,
|
|
1688
|
+
// not a 'picker'/'confirm'. A modal whose kind is unknown (legacy
|
|
1689
|
+
// adapter, or a spec that predates modal_kind) reads as 'approval'
|
|
1690
|
+
// so genuine approvals keep auto-approving; only an explicit
|
|
1691
|
+
// 'picker'/'confirm' is excluded here.
|
|
1692
|
+
// (2) structural anchor — a real approval offers an affirmative AND a
|
|
1693
|
+
// decline option (pickApprovalButton finds a positive that isn't a
|
|
1694
|
+
// decline, and hasNegativeApprovalOption confirms a No/Cancel/Deny
|
|
1695
|
+
// is present). A model picker ("1. Default 2. Opus 3. Sonnet")
|
|
1696
|
+
// has no decline, so even an un-migrated picker is caught here.
|
|
1697
|
+
//
|
|
1698
|
+
// Mirrors the SDK v1 detect-status approval heuristic (detect-status.ts).
|
|
1699
|
+
const modalKind = typeof modal?.kind === 'string' ? modal.kind : 'approval';
|
|
1700
|
+
if (modalKind !== 'approval') {
|
|
1701
|
+
// Picker/confirm — leave it for the user; keep the modal surfaced.
|
|
1702
|
+
return autoApproveActive;
|
|
1703
|
+
}
|
|
1704
|
+
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(buttons, this.provider);
|
|
1705
|
+
if (buttonIndex < 0 || !hasNegativeApprovalOption(buttons)) {
|
|
1706
|
+
// No affirmative matched, or no decline option present (→ not a real
|
|
1707
|
+
// consent prompt, e.g. a picker that slipped past the kind gate).
|
|
1708
|
+
// Surface the modal so the user decides; never pick blindly.
|
|
1709
|
+
return autoApproveActive;
|
|
1710
|
+
}
|
|
1711
|
+
// Modal *identity* signature — the question/button set only, NO volatile
|
|
1712
|
+
// counters. This is what the settle gate tracks: the FSM bumps
|
|
1713
|
+
// approvalEntrySeq on every fresh waiting_approval entry, and a
|
|
1714
|
+
// modal→generating→modal flap (the question line scrolled out of the
|
|
1715
|
+
// captured frame while the button block stays) re-enters and bumps it
|
|
1716
|
+
// again. Folding that seq into the settle signature made the 600ms
|
|
1717
|
+
// settle clock restart on every flap, so the modal never stayed stable
|
|
1718
|
+
// long enough to fire — the gate was never satisfied. Identity excludes
|
|
1719
|
+
// the seq so button/seq flap of the SAME modal keeps one settle clock.
|
|
1720
|
+
const modalSignature = [
|
|
1721
|
+
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1722
|
+
buttons.join('|'),
|
|
1723
|
+
buttonIndex,
|
|
1724
|
+
].join('::');
|
|
1725
|
+
// Busy-window re-entry guard still needs the seq: two DISTINCT
|
|
1726
|
+
// back-to-back approvals can carry identical message/buttons (common
|
|
1727
|
+
// with claude-cli). Without the seq their busy signatures collide and
|
|
1728
|
+
// the 5s busy-window guard below would swallow the second auto-approve,
|
|
1729
|
+
// leaving it stuck. The seq is bumped by the FSM on every fresh
|
|
1730
|
+
// waiting_approval entry, so a new approval always yields a new busy
|
|
1731
|
+
// signature and fires through.
|
|
1732
|
+
const approvalEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
|
|
1733
|
+
? adapterStatus.approvalEntrySeq
|
|
1734
|
+
: 0;
|
|
1735
|
+
const busySignature = `${approvalEntrySeq}::${modalSignature}`;
|
|
1736
|
+
// Already fired for this exact modal entry and still inside the busy
|
|
1737
|
+
// window — nothing to do (re-entry guard for repeated snapshots of one
|
|
1738
|
+
// modal).
|
|
1739
|
+
if (this.autoApproveBusy && busySignature === this.lastAutoApprovalSignature) {
|
|
1740
|
+
return autoApproveActive;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Settle gate: only fire once this modal identity has been stable for
|
|
1744
|
+
// AUTO_APPROVE_SETTLE_MS. A still-streaming prompt mutates its
|
|
1745
|
+
// message/buttons each frame → new identity → clock restarts, so we
|
|
1746
|
+
// never approve a half-rendered prompt (the "resolves too fast" bug).
|
|
1747
|
+
if (modalSignature !== this.pendingAutoApprovalSignature) {
|
|
1748
|
+
this.pendingAutoApprovalSignature = modalSignature;
|
|
1749
|
+
this.pendingAutoApprovalSince = now;
|
|
869
1750
|
}
|
|
1751
|
+
const settledForMs = now - this.pendingAutoApprovalSince;
|
|
1752
|
+
if (settledForMs < CliProviderInstance.AUTO_APPROVE_SETTLE_MS) {
|
|
1753
|
+
// Not yet settled. Arm a timer to re-check after the remaining quiet
|
|
1754
|
+
// window — the PTY may go silent once the prompt finishes painting,
|
|
1755
|
+
// so there is no guaranteed status-change frame to re-drive us.
|
|
1756
|
+
if (this.autoApproveSettleTimer) clearTimeout(this.autoApproveSettleTimer);
|
|
1757
|
+
this.autoApproveSettleTimer = setTimeout(() => {
|
|
1758
|
+
this.autoApproveSettleTimer = null;
|
|
1759
|
+
this.recheckAutoApproveSettled();
|
|
1760
|
+
}, CliProviderInstance.AUTO_APPROVE_SETTLE_MS - settledForMs + 20);
|
|
1761
|
+
return autoApproveActive;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// Settled — fire the approve key.
|
|
1765
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
1766
|
+
this.autoApproveBusy = true;
|
|
1767
|
+
this.lastAutoApprovalSignature = busySignature;
|
|
1768
|
+
this.pendingAutoApprovalSignature = '';
|
|
1769
|
+
this.pendingAutoApprovalSince = 0;
|
|
1770
|
+
this.autoApproveInactiveSince = 0;
|
|
1771
|
+
if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
|
|
1772
|
+
this.autoApproveBusyTimer = setTimeout(() => {
|
|
1773
|
+
this.autoApproveBusy = false;
|
|
1774
|
+
this.autoApproveBusyTimer = null;
|
|
1775
|
+
this.lastAutoApprovalSignature = '';
|
|
1776
|
+
}, 5000);
|
|
1777
|
+
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
1778
|
+
setTimeout(() => {
|
|
1779
|
+
this.adapter.resolveModal(buttonIndex);
|
|
1780
|
+
}, 0);
|
|
870
1781
|
return autoApproveActive;
|
|
871
1782
|
}
|
|
872
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* Re-drive the auto-approve check after the settle quiet window elapses.
|
|
1786
|
+
* The PTY may have gone silent once the approval prompt finished painting,
|
|
1787
|
+
* so no status-change frame is guaranteed to re-enter maybeAutoApproveStatus
|
|
1788
|
+
* — this timer-driven re-check picks up the now-settled modal and fires.
|
|
1789
|
+
* Deliberately lighter than detectStatusTransition(): it only re-evaluates
|
|
1790
|
+
* the approval decision; the next real PTY frame refreshes visible status.
|
|
1791
|
+
*/
|
|
1792
|
+
private recheckAutoApproveSettled(): void {
|
|
1793
|
+
try {
|
|
1794
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1795
|
+
this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
1796
|
+
} catch { /* adapter gone / transient — next frame retries */ }
|
|
1797
|
+
}
|
|
1798
|
+
|
|
873
1799
|
private detectStatusTransition(): void {
|
|
874
1800
|
const now = Date.now();
|
|
875
1801
|
// Status-change handling is a hot path: PTY output can fire it many times
|
|
876
1802
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
877
1803
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
878
1804
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1805
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
1806
|
+
this.provider,
|
|
1807
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
1808
|
+
);
|
|
1809
|
+
if (adapterProviderSessionId) {
|
|
1810
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
1811
|
+
}
|
|
879
1812
|
const parsedStatus = null;
|
|
880
1813
|
const rawStatus = adapterStatus.status;
|
|
881
1814
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
882
|
-
|
|
883
|
-
|
|
1815
|
+
// During the autoApproveBusy window (2s after firing approval key), the PTY
|
|
1816
|
+
// can briefly report 'idle' before the next generating phase starts. Treat that
|
|
1817
|
+
// transient idle as 'generating' to suppress a spurious agent:generating_completed
|
|
1818
|
+
// push notification. The adapter's status is otherwise authoritative — native
|
|
1819
|
+
// transcript shape does NOT override the FSM's busy/idle decision.
|
|
1820
|
+
const autoApproveHoldIdle = this.autoApproveBusy && rawStatus === 'idle';
|
|
1821
|
+
const newStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus;
|
|
1822
|
+
const dirName = workingDirBasename(this.workingDir);
|
|
884
1823
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
885
1824
|
const partial = this.adapter.getPartialResponse();
|
|
1825
|
+
// Liveness fingerprint for the no-progress watchdog. The parsed
|
|
1826
|
+
// assistant buffer (`partial`) alone goes static while a tool/build runs
|
|
1827
|
+
// — the assistant emits no tokens even though the PTY is actively
|
|
1828
|
+
// printing tool output — which made the watchdog false-fire a "stuck"
|
|
1829
|
+
// alert mid-turn. Fold in the adapter's raw-activity timestamps so any
|
|
1830
|
+
// visible terminal progress (lastScreenChangeAt) or raw PTY byte
|
|
1831
|
+
// (lastOutputAt) keeps the fingerprint moving. The watchdog then only
|
|
1832
|
+
// survives a genuine stall where nothing at all is happening.
|
|
886
1833
|
const progressFingerprint = newStatus === 'generating'
|
|
887
|
-
? `${partial || ''}`.slice(-2000)
|
|
1834
|
+
? `${`${partial || ''}`.slice(-2000)}::scr=${adapterStatus.lastScreenChangeAt ?? 0}::out=${adapterStatus.lastOutputAt ?? 0}`
|
|
888
1835
|
: undefined;
|
|
889
1836
|
|
|
890
1837
|
const previousStatus = this.lastStatus;
|
|
891
1838
|
if (newStatus !== this.lastStatus) {
|
|
892
1839
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
893
1840
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
1841
|
+
// If a completion event is already pending and the turn has ended
|
|
1842
|
+
// (generatingStartedAt===0), the PTY is painting its prompt area
|
|
1843
|
+
// after completing. Ignore this blip — do not cancel the pending
|
|
1844
|
+
// completion and do not advance lastStatus to generating.
|
|
1845
|
+
if (this.completedDebouncePending && this.generatingStartedAt === 0) {
|
|
1846
|
+
LOG.debug('CLI', `[${this.type}] ignoring post-completion PTY generating blip (generatingStartedAt=0)`);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
894
1849
|
this.suppressIdleHistoryReplay = false;
|
|
895
1850
|
// Cancel any pending completed event (multi-step: idle→generating resume)
|
|
896
1851
|
if (this.completedDebouncePending) {
|
|
897
|
-
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating)`);
|
|
1852
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating) generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse}`);
|
|
898
1853
|
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
899
1854
|
this.completedDebouncePending = null;
|
|
900
1855
|
}
|
|
@@ -926,10 +1881,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
926
1881
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
927
1882
|
const modal = adapterStatus.activeModal;
|
|
928
1883
|
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1884
|
+
// Include the FSM's approval entry seq, mirroring the auto-approve
|
|
1885
|
+
// path (maybeAutoApproveStatus) and resolveModal's sameEntryReResolve
|
|
1886
|
+
// guard. Two distinct back-to-back approvals can carry identical
|
|
1887
|
+
// message/buttons (very common with claude-cli's "Allow Bash
|
|
1888
|
+
// command?"). Without the seq their fingerprints collide and the dedup
|
|
1889
|
+
// below silently drops the second waiting_approval event — it is never
|
|
1890
|
+
// emitted, so it cannot even land in the pending inbox for a later
|
|
1891
|
+
// read_chat reconcile to recover. The seq is bumped by the FSM on every
|
|
1892
|
+
// fresh waiting_approval entry, so a new approval always yields a new
|
|
1893
|
+
// fingerprint and emits.
|
|
1894
|
+
const approvalEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
|
|
1895
|
+
? adapterStatus.approvalEntrySeq
|
|
1896
|
+
: 0;
|
|
1897
|
+
const approvalFingerprint = JSON.stringify({
|
|
1898
|
+
message: typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1899
|
+
buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
|
|
1900
|
+
seq: approvalEntrySeq,
|
|
1901
|
+
});
|
|
1902
|
+
// PTY redraws repeat the same modal content; fingerprint dedup prevents duplicate events.
|
|
1903
|
+
// Do NOT also gate on lastStatus: consecutive approvals can arrive waiting_approval→waiting_approval
|
|
1904
|
+
// (e.g. antigravity-cli resolves one prompt and immediately shows the next) and would be silently dropped.
|
|
1905
|
+
if (approvalFingerprint !== this.lastApprovalEventFingerprint) {
|
|
1906
|
+
this.lastApprovalEventFingerprint = approvalFingerprint;
|
|
933
1907
|
this.appendRuntimeSystemMessage(
|
|
934
1908
|
this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
|
|
935
1909
|
`approval_request:${now}`,
|
|
@@ -941,22 +1915,118 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
941
1915
|
modalButtons: modal?.buttons,
|
|
942
1916
|
});
|
|
943
1917
|
}
|
|
1918
|
+
} else if (newStatus === 'generating' && this.lastStatus === 'waiting_approval') {
|
|
1919
|
+
// Approval resolved and the agent resumed work. Defense-in-depth:
|
|
1920
|
+
// clear the approval emit fingerprint here too (not only on
|
|
1921
|
+
// completion at scheduleCompletedDebounceFlush). A subsequent
|
|
1922
|
+
// waiting_approval with the same modal content as the one just
|
|
1923
|
+
// resolved would otherwise collide with the stale fingerprint and be
|
|
1924
|
+
// dropped. The seq in the fingerprint already separates entries; this
|
|
1925
|
+
// reset is a belt-and-suspenders guard for the re-entry case.
|
|
1926
|
+
this.lastApprovalEventFingerprint = '';
|
|
944
1927
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
945
1928
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
946
|
-
//
|
|
1929
|
+
// Guard: if generatingStartedAt===0 and no debounce pending, the generating phase
|
|
1930
|
+
// was entered from 'starting' state (startup PTY noise), not from a real idle→generating
|
|
1931
|
+
// task dispatch. The idle→generating handler is the only code path that sets
|
|
1932
|
+
// generatingStartedAt and generatingDebouncePending, so both being absent means no
|
|
1933
|
+
// task was ever dispatched. Suppress the spurious completion event and fall through
|
|
1934
|
+
// to a simple lastStatus update.
|
|
1935
|
+
if (!this.generatingStartedAt && !this.generatingDebouncePending) {
|
|
1936
|
+
LOG.debug('CLI', `[${this.type}] suppressed startup-phase generating→idle blip (generatingStartedAt=0, no debounce pending)`);
|
|
1937
|
+
} else
|
|
1938
|
+
// If debounce still pending (generating lasted < 1s), cancel both UI events.
|
|
1939
|
+
// Still emit agent:generating_completed so mesh orchestration can record
|
|
1940
|
+
// task_completed for direct dispatches that complete faster than the debounce.
|
|
947
1941
|
if (this.generatingDebouncePending) {
|
|
948
|
-
|
|
1942
|
+
const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
|
|
1943
|
+
LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
|
|
949
1944
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1945
|
+
// Emit completion for mesh task association even though the UI generating
|
|
1946
|
+
// started/completed pair is suppressed (too short for visible UI update).
|
|
1947
|
+
let shortFinalSummary: string | undefined;
|
|
1948
|
+
let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
|
|
1949
|
+
try {
|
|
1950
|
+
const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
|
|
1951
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1952
|
+
shortEvidenceSource = evidence.source;
|
|
1953
|
+
shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1954
|
+
} catch { /* best-effort */ }
|
|
1955
|
+
// If a real response is confirmed, retroactively emit started so the chat
|
|
1956
|
+
// bubble appears even though the debounce suppressed the original event.
|
|
1957
|
+
if (shortFinalSummary) {
|
|
1958
|
+
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now - shortDurationMs });
|
|
1959
|
+
}
|
|
950
1960
|
this.generatingDebouncePending = null;
|
|
951
1961
|
this.generatingStartedAt = 0;
|
|
1962
|
+
const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary;
|
|
1963
|
+
if (missingEvidence) {
|
|
1964
|
+
LOG.warn('CLI', `[${this.type}] short completion missing final assistant evidence (source=${shortEvidenceSource})`);
|
|
1965
|
+
}
|
|
1966
|
+
// When evidence is missing and there is no active mesh task context, suppress
|
|
1967
|
+
// the completion event. Providers with requiresFinalAssistantBeforeIdle or
|
|
1968
|
+
// external-native history must confirm a final assistant message before the
|
|
1969
|
+
// coordinator records task_completed. Only emit here if a mesh task is active
|
|
1970
|
+
// so the coordinator can apply its own timeout/retry logic.
|
|
1971
|
+
const hasMeshContext = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1972
|
+
if (missingEvidence && !hasMeshContext) {
|
|
1973
|
+
LOG.info('CLI', `[${this.type}] short completion suppressed: missing final assistant evidence, no mesh context (source=${shortEvidenceSource})`);
|
|
1974
|
+
// completedDebouncePending intentionally left null — the session is now idle
|
|
1975
|
+
// with no confirmed turn, matching the startup-blip suppression semantics.
|
|
1976
|
+
// (No EvtTrace: not a mesh session, so nothing routes to a coordinator.)
|
|
1977
|
+
} else {
|
|
1978
|
+
// EVTTRACE: completion fired (short-generating idle path).
|
|
1979
|
+
if (this.isMeshWorkerSession()) {
|
|
1980
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), `short-generating idle (source=${shortEvidenceSource})`);
|
|
1981
|
+
}
|
|
1982
|
+
this.pushEvent({
|
|
1983
|
+
event: 'agent:generating_completed',
|
|
1984
|
+
chatTitle,
|
|
1985
|
+
duration: 0,
|
|
1986
|
+
timestamp: now,
|
|
1987
|
+
finalSummary: shortFinalSummary,
|
|
1988
|
+
completionDiagnostic: {
|
|
1989
|
+
reason: 'short_generating_suppressed',
|
|
1990
|
+
shortDurationMs,
|
|
1991
|
+
finalAssistantEvidenceSource: shortEvidenceSource,
|
|
1992
|
+
...(missingEvidence ? { blockReason: 'missing_final_assistant' } : {}),
|
|
1993
|
+
},
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
952
1996
|
} else {
|
|
953
1997
|
// Debounce completed, then require the rich transcript path that read_chat
|
|
954
1998
|
// uses to show an idle turn whose last user-facing message is assistant.
|
|
955
|
-
this.completedDebouncePending = {
|
|
956
|
-
|
|
1999
|
+
this.completedDebouncePending = {
|
|
2000
|
+
chatTitle,
|
|
2001
|
+
duration,
|
|
2002
|
+
timestamp: now,
|
|
2003
|
+
firstObservedAt: now,
|
|
2004
|
+
previousStatus: this.lastStatus,
|
|
2005
|
+
};
|
|
2006
|
+
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
2007
|
+
const flushDelay = ownsExternalHistory ? 0 : 3000;
|
|
2008
|
+
LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
|
|
2009
|
+
this.scheduleCompletedDebounceFlush(flushDelay);
|
|
957
2010
|
}
|
|
958
2011
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
2012
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
2013
|
+
} else if (newStatus === 'error') {
|
|
2014
|
+
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
2015
|
+
this.generatingDebouncePending = null;
|
|
2016
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
2017
|
+
this.completedDebouncePending = null;
|
|
2018
|
+
this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
|
|
2019
|
+
this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
|
|
2020
|
+
this.pushEvent({
|
|
2021
|
+
event: 'agent:stopped',
|
|
2022
|
+
chatTitle,
|
|
2023
|
+
timestamp: now,
|
|
2024
|
+
finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
|
|
2025
|
+
completionDiagnostic: {
|
|
2026
|
+
reason: adapterStatus.errorReason || 'provider_error',
|
|
2027
|
+
errorMessage: adapterStatus.errorMessage || undefined,
|
|
2028
|
+
},
|
|
2029
|
+
});
|
|
960
2030
|
} else if (newStatus === 'stopped') {
|
|
961
2031
|
// Cancel any pending debounce
|
|
962
2032
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -976,8 +2046,43 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
976
2046
|
|
|
977
2047
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
978
2048
|
const agentKey = `${this.type}:cli`;
|
|
979
|
-
|
|
2049
|
+
// Approval pending is detected from the raw adapter status, not `newStatus`:
|
|
2050
|
+
// auto-approve synthesizes `waiting_approval` → 'generating', which would
|
|
2051
|
+
// otherwise let the no-progress watchdog accumulate the approval wait.
|
|
2052
|
+
const approvalPending = rawStatus === 'waiting_approval';
|
|
2053
|
+
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint, approvalPending);
|
|
2054
|
+
const monitorParsedStatus: any = parsedStatus;
|
|
980
2055
|
for (const me of monitorEvents) {
|
|
2056
|
+
if (
|
|
2057
|
+
me.type === 'monitor:no_progress'
|
|
2058
|
+
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
2059
|
+
&& !this.hasAdapterPendingResponse()
|
|
2060
|
+
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
2061
|
+
) {
|
|
2062
|
+
// EVTTRACE: completion fired (no-progress monitor reconciled to completion).
|
|
2063
|
+
if (this.isMeshWorkerSession()) {
|
|
2064
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), 'no_progress_monitor_final_summary');
|
|
2065
|
+
}
|
|
2066
|
+
this.pushEvent({
|
|
2067
|
+
event: 'agent:generating_completed',
|
|
2068
|
+
chatTitle,
|
|
2069
|
+
duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
|
|
2070
|
+
timestamp: me.timestamp,
|
|
2071
|
+
finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
|
|
2072
|
+
completionDiagnostic: {
|
|
2073
|
+
providerType: this.type,
|
|
2074
|
+
sessionId: this.instanceId,
|
|
2075
|
+
providerSessionId: this.providerSessionId || null,
|
|
2076
|
+
reconciliationReason: 'no_progress_monitor_final_summary',
|
|
2077
|
+
finalAssistantPresent: true,
|
|
2078
|
+
},
|
|
2079
|
+
});
|
|
2080
|
+
this.generatingStartedAt = 0;
|
|
2081
|
+
// Cancel any pending debounce flush — monitor already fired completion.
|
|
2082
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
2083
|
+
this.completedDebouncePending = null;
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
981
2086
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
982
2087
|
}
|
|
983
2088
|
}
|
|
@@ -997,15 +2102,49 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
997
2102
|
workspaceName: typeof event.workspaceName === 'string' && event.workspaceName.trim()
|
|
998
2103
|
? event.workspaceName
|
|
999
2104
|
: this.workingDir,
|
|
2105
|
+
// Carry the workspace under BOTH `workspace` and `workspaceName` so the
|
|
2106
|
+
// downstream mesh forward/merge path — which reads `workspace` — can
|
|
2107
|
+
// propagate it to the coordinator snapshot. Without `workspace` the live
|
|
2108
|
+
// event path delivers an empty workspace and the dashboard falls back to
|
|
2109
|
+
// the generic "Terminal (Mesh Node)" title.
|
|
2110
|
+
workspace: typeof event.workspace === 'string' && event.workspace.trim()
|
|
2111
|
+
? event.workspace
|
|
2112
|
+
: this.workingDir,
|
|
1000
2113
|
providerSessionId: typeof event.providerSessionId === 'string' && event.providerSessionId.trim()
|
|
1001
2114
|
? event.providerSessionId
|
|
1002
2115
|
: this.providerSessionId,
|
|
1003
2116
|
};
|
|
2117
|
+
// TASKIDLESS: stamp the mesh task primary key on lifecycle events emitted by
|
|
2118
|
+
// a mesh worker session. The consumer (updateDirectDispatchStatus) was switched
|
|
2119
|
+
// to key on task_id (CANON-B), but the producer never carried it — so every
|
|
2120
|
+
// forwarded metadataEvent.taskId arrived undefined and the coordinator fell back
|
|
2121
|
+
// to a session_id match, which can flip a sibling dispatch row. The session
|
|
2122
|
+
// already knows its own taskId via attachMeshAssignment (settings.meshActiveTaskId);
|
|
2123
|
+
// surface it here so updateDirectDispatchStatus hits the exact PK row and the
|
|
2124
|
+
// session_id fallback is never exercised. Non-mesh sessions get no taskId
|
|
2125
|
+
// (regression guard) — isMeshWorkerSession() gates the injection.
|
|
2126
|
+
if (this.isMeshWorkerSession() && this.settings.meshActiveTaskId) {
|
|
2127
|
+
const existingTaskId = typeof enrichedEvent.taskId === 'string' && enrichedEvent.taskId.trim()
|
|
2128
|
+
? enrichedEvent.taskId
|
|
2129
|
+
: undefined;
|
|
2130
|
+
if (!existingTaskId) {
|
|
2131
|
+
enrichedEvent.taskId = this.settings.meshActiveTaskId;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
1004
2134
|
if (this.context?.emitProviderEvent) {
|
|
1005
2135
|
this.context.emitProviderEvent(enrichedEvent);
|
|
1006
|
-
|
|
2136
|
+
} else {
|
|
2137
|
+
this.events.push(enrichedEvent);
|
|
2138
|
+
}
|
|
2139
|
+
// Auto-detach a direct-dispatch mesh assignment once the dispatched
|
|
2140
|
+
// task reaches a terminal state. Leaving meshNodeFor pinned would
|
|
2141
|
+
// route this session's next unrelated turn (a dashboard chat) into
|
|
2142
|
+
// the coordinator as if it were the completion of another task.
|
|
2143
|
+
// We schedule after the emit so the originating coordinator still
|
|
2144
|
+
// observes the completion event with its routing marker intact.
|
|
2145
|
+
if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
|
|
2146
|
+
try { this.detachMeshAssignment(); } catch { /* best-effort */ }
|
|
1007
2147
|
}
|
|
1008
|
-
this.events.push(enrichedEvent);
|
|
1009
2148
|
}
|
|
1010
2149
|
|
|
1011
2150
|
private flushEvents(): ProviderEvent[] {
|
|
@@ -1151,6 +2290,25 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1151
2290
|
return false;
|
|
1152
2291
|
}
|
|
1153
2292
|
|
|
2293
|
+
/** @see ProviderInstance.noteManualInteraction */
|
|
2294
|
+
noteManualInteraction(now = Date.now()): void {
|
|
2295
|
+
this.manualAttendance.note(now);
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* Whether auto-approve should be treated as active *right now* for display
|
|
2300
|
+
* and firing decisions: the configured intent AND the user is not currently
|
|
2301
|
+
* attending this session by hand. When a human is attending, auto-approve is
|
|
2302
|
+
* held so the modal stays visible and they can drive it via the controlbar.
|
|
2303
|
+
* Provider-agnostic — the attendance signal is the command set, never any
|
|
2304
|
+
* CLI-specific modal text.
|
|
2305
|
+
*/
|
|
2306
|
+
private autoApproveEffectivelyActive(status: string | undefined, now = Date.now()): boolean {
|
|
2307
|
+
return status === 'waiting_approval'
|
|
2308
|
+
&& this.shouldAutoApprove()
|
|
2309
|
+
&& !this.manualAttendance.isAttended(now);
|
|
2310
|
+
}
|
|
2311
|
+
|
|
1154
2312
|
private recordAutoApproval(modalMessage?: string, buttonLabel?: string, now = Date.now()): void {
|
|
1155
2313
|
this.appendRuntimeSystemMessage(
|
|
1156
2314
|
formatAutoApprovalMessage(modalMessage, buttonLabel),
|
|
@@ -1238,7 +2396,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1238
2396
|
receivedAt: normalizedMessage.receivedAt || normalizedMessage.timestamp,
|
|
1239
2397
|
historyDedupKey: dedupKey,
|
|
1240
2398
|
}],
|
|
1241
|
-
this.adapter.getScriptParsedStatus?.()?.title || this.workingDir
|
|
2399
|
+
this.adapter.getScriptParsedStatus?.()?.title || workingDirBasename(this.workingDir),
|
|
1242
2400
|
this.instanceId,
|
|
1243
2401
|
this.providerSessionId,
|
|
1244
2402
|
);
|
|
@@ -1258,12 +2416,28 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1258
2416
|
index,
|
|
1259
2417
|
source: 'parsed',
|
|
1260
2418
|
}));
|
|
2419
|
+
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
2420
|
+
? message.role.trim().toLowerCase()
|
|
2421
|
+
: '';
|
|
1261
2422
|
const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
|
|
1262
2423
|
message: entry.message,
|
|
1263
2424
|
index: parsedMessages.length + index,
|
|
1264
|
-
source: 'runtime',
|
|
2425
|
+
source: 'runtime' as const,
|
|
1265
2426
|
runtimeKey: entry.key,
|
|
1266
|
-
}))
|
|
2427
|
+
})).filter((entry) => {
|
|
2428
|
+
const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
|
|
2429
|
+
? entry.message.meta as Record<string, unknown>
|
|
2430
|
+
: {};
|
|
2431
|
+
if (meta.runtimeInputAck !== true) return true;
|
|
2432
|
+
const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
|
|
2433
|
+
if (!runtimeText) return false;
|
|
2434
|
+
return !parsedEntries.some((parsedEntry) => {
|
|
2435
|
+
const parsedRole = getRole(parsedEntry.message);
|
|
2436
|
+
if (parsedRole !== 'user' && parsedRole !== 'human') return false;
|
|
2437
|
+
const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
|
|
2438
|
+
return parsedText === runtimeText;
|
|
2439
|
+
});
|
|
2440
|
+
});
|
|
1267
2441
|
const getTime = (message: ChatMessage): number => {
|
|
1268
2442
|
const value = typeof message.receivedAt === 'number'
|
|
1269
2443
|
? message.receivedAt
|
|
@@ -1273,9 +2447,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1273
2447
|
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1274
2448
|
};
|
|
1275
2449
|
|
|
1276
|
-
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1277
|
-
? message.role.trim().toLowerCase()
|
|
1278
|
-
: '';
|
|
1279
2450
|
const isRuntimeOverlay = (entry: MergeEntry): boolean => {
|
|
1280
2451
|
if (entry.source !== 'runtime') return false;
|
|
1281
2452
|
const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
|
|
@@ -1341,7 +2512,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
2512
|
this.providerSessionId = nextSessionId;
|
|
1342
2513
|
this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
|
|
1343
2514
|
this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
|
|
1344
|
-
this.
|
|
2515
|
+
if (this.shouldHydrateExistingProviderHistory()) {
|
|
2516
|
+
this.restorePersistedHistoryFromCurrentSession();
|
|
2517
|
+
}
|
|
1345
2518
|
this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
|
|
1346
2519
|
this.onProviderSessionResolved?.({
|
|
1347
2520
|
instanceId: this.instanceId,
|
|
@@ -1354,13 +2527,39 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1354
2527
|
LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
|
|
1355
2528
|
}
|
|
1356
2529
|
|
|
1357
|
-
private
|
|
2530
|
+
private shouldHydrateExistingProviderHistory(): boolean {
|
|
2531
|
+
return this.launchMode === 'resume' || this.launchMode === 'manual';
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
|
|
2535
|
+
if (this.launchMode !== 'new') return false;
|
|
2536
|
+
if (this.providerSessionId) return false;
|
|
2537
|
+
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
|
|
2538
|
+
if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
|
|
2539
|
+
if (parsedProviderSessionId) return true;
|
|
2540
|
+
|
|
2541
|
+
const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
|
|
2542
|
+
|
|
2543
|
+
// Untimestamped idle parser output during a fresh launch is usually the
|
|
2544
|
+
// provider's last workspace transcript before a new turn exists.
|
|
2545
|
+
return newestMessageAt === 0;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
private syncCanonicalSavedHistoryIfNeeded(options: { full?: boolean } = {}): boolean {
|
|
1358
2549
|
if (!this.providerSessionId) return false;
|
|
1359
|
-
const canonicalHistory = this.provider.
|
|
2550
|
+
const canonicalHistory = this.provider.nativeHistory;
|
|
1360
2551
|
if (!canonicalHistory) return false;
|
|
1361
2552
|
|
|
2553
|
+
// Per-status-report hydration reads only a bounded tail (snapshot needs at
|
|
2554
|
+
// most the newest 60). The once-per-resume restore path passes full:true
|
|
2555
|
+
// because seedSessionHistory needs the COMPLETE transcript to seed dedup
|
|
2556
|
+
// state. The read-cache key encodes the window so the bounded and full
|
|
2557
|
+
// reads don't share/clobber each other's 2s cache entry.
|
|
2558
|
+
const limit = options.full ? Number.MAX_SAFE_INTEGER : STATUS_HYDRATION_TAIL_LIMIT;
|
|
2559
|
+
const windowTag = options.full ? 'full' : `tail:${STATUS_HYDRATION_TAIL_LIMIT}`;
|
|
2560
|
+
|
|
1362
2561
|
if (isNativeSourceCanonicalHistory(canonicalHistory)) {
|
|
1363
|
-
const cacheKey = [this.type, this.providerSessionId, this.workingDir].join('\0');
|
|
2562
|
+
const cacheKey = [this.type, this.providerSessionId, this.workingDir, windowTag].join('\0');
|
|
1364
2563
|
const now = Date.now();
|
|
1365
2564
|
if (cacheKey === this.lastNativeSourceCanonicalCacheKey && now - this.lastNativeSourceCanonicalCheckAt < 2_000) {
|
|
1366
2565
|
return true;
|
|
@@ -1373,7 +2572,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1373
2572
|
historySessionId: this.providerSessionId,
|
|
1374
2573
|
workspace: this.workingDir,
|
|
1375
2574
|
offset: 0,
|
|
1376
|
-
limit
|
|
2575
|
+
limit,
|
|
1377
2576
|
historyBehavior: this.provider.historyBehavior,
|
|
1378
2577
|
scripts: this.provider.scripts as any,
|
|
1379
2578
|
});
|
|
@@ -1390,7 +2589,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1390
2589
|
}
|
|
1391
2590
|
|
|
1392
2591
|
try {
|
|
1393
|
-
const cacheKey = [this.type, this.providerSessionId, this.workingDir, canonicalHistory.mode || 'materialized-mirror'].join('\0');
|
|
2592
|
+
const cacheKey = [this.type, this.providerSessionId, this.workingDir, canonicalHistory.mode || 'materialized-mirror', windowTag].join('\0');
|
|
1394
2593
|
const now = Date.now();
|
|
1395
2594
|
if (cacheKey === this.lastNativeSourceCanonicalCacheKey && now - this.lastNativeSourceCanonicalCheckAt < 2_000) {
|
|
1396
2595
|
return true;
|
|
@@ -1401,7 +2600,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1401
2600
|
if (!materializeProviderNativeHistory(this.type, canonicalHistory, this.providerSessionId, this.workingDir, this.provider.scripts as any)) {
|
|
1402
2601
|
return false;
|
|
1403
2602
|
}
|
|
1404
|
-
|
|
2603
|
+
// Bounded by default: the per-status-report path only needs the newest
|
|
2604
|
+
// STATUS_HYDRATION_TAIL_LIMIT messages because the snapshot caps
|
|
2605
|
+
// activeChat.messages to the last 60 (status/normalize.ts) and loads
|
|
2606
|
+
// the rest lazily via read_chat on subscribe. The once-per-resume
|
|
2607
|
+
// restore path passes full:true so seedSessionHistory still sees the
|
|
2608
|
+
// COMPLETE transcript for prefix-dedup seeding. readChatHistory serves
|
|
2609
|
+
// a bounded limit as an O(tail) read.
|
|
2610
|
+
const restoredHistory = readChatHistory(this.type, 0, limit, this.providerSessionId, 0, this.provider.historyBehavior);
|
|
1405
2611
|
this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
|
|
1406
2612
|
role: message.role,
|
|
1407
2613
|
content: message.content,
|
|
@@ -1417,10 +2623,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1417
2623
|
|
|
1418
2624
|
private restorePersistedHistoryFromCurrentSession(): void {
|
|
1419
2625
|
if (!this.providerSessionId) return;
|
|
1420
|
-
|
|
1421
|
-
|
|
2626
|
+
// Restore is the once-per-resume seeding path: it needs the COMPLETE
|
|
2627
|
+
// transcript so seedSessionHistory can prime dedup state. Pass full so the
|
|
2628
|
+
// hydration read is unbounded here (and only here).
|
|
2629
|
+
this.syncCanonicalSavedHistoryIfNeeded({ full: true });
|
|
2630
|
+
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
|
|
1422
2631
|
? readProviderChatHistory(this.type, {
|
|
1423
|
-
canonicalHistory: this.provider.
|
|
2632
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1424
2633
|
historySessionId: this.providerSessionId,
|
|
1425
2634
|
workspace: this.workingDir,
|
|
1426
2635
|
offset: 0,
|