@adhdev/daemon-core 0.9.82-rc.37 → 0.9.82-rc.371
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/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +116 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +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 +46565 -19737
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +43563 -16868
- 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 +37 -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/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +120 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router.ts +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
|
@@ -12,18 +12,55 @@ import type { CliAdapter } from '../cli-adapter-types.js';
|
|
|
12
12
|
import { flattenContent, normalizeInputEnvelope, type InputEnvelope, type ProviderModule, type ProviderScripts } from '../providers/contracts.js';
|
|
13
13
|
import { assertProviderSupportsDeclaredInput, assertTextOnlyInput } from '../providers/provider-input-support.js';
|
|
14
14
|
import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
|
|
15
|
+
import { pickApprovalButton } from '../providers/approval-utils.js';
|
|
15
16
|
import type { ProviderInstance } from '../providers/provider-instance.js';
|
|
16
|
-
import { readProviderChatHistory } from '../config/chat-history.js';
|
|
17
|
+
import { isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
|
|
18
|
+
import { getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
17
19
|
import { LOG, getRecentLogs } from '../logging/logger.js';
|
|
18
20
|
import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
|
|
19
21
|
import { buildChatMessageSignature, hashSignatureParts } from '../chat/chat-signatures.js';
|
|
22
|
+
import {
|
|
23
|
+
CHAT_SOURCE_REGISTRY,
|
|
24
|
+
buildV1NativePresentObservation,
|
|
25
|
+
chatSourceSessionKey,
|
|
26
|
+
type ChatSourceDecision,
|
|
27
|
+
type ChatSourceObservation,
|
|
28
|
+
type ChatSourceTransitionCause,
|
|
29
|
+
} from '../chat/source-resolver.js';
|
|
20
30
|
import type { ChatMessage } from '../types.js';
|
|
21
31
|
import type { SessionTransport } from '../shared-types.js';
|
|
22
|
-
import { filterUserFacingChatMessages, normalizeChatMessages } from '../providers/chat-message-normalization.js';
|
|
32
|
+
import { filterUserFacingChatMessages, isActivityChatMessage, isUserFacingChatMessage, normalizeChatMessages } from '../providers/chat-message-normalization.js';
|
|
33
|
+
import { normalizeMeshWorkspaceForCompare } from '@adhdev/mesh-shared';
|
|
23
34
|
|
|
24
35
|
const RECENT_SEND_WINDOW_MS = 1200;
|
|
25
36
|
export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
|
|
37
|
+
// Minimum tail floor for hot-path history/mirror reads. The dashboard requests a
|
|
38
|
+
// bounded tail (~60); we keep a small floor so a tiny requested tailLimit still
|
|
39
|
+
// has enough surrounding context for seed/mirror dedup correctness, but it must
|
|
40
|
+
// NOT dominate the hot subscribe/poll path the way the previous 200 floor did.
|
|
41
|
+
// readChatHistory now serves this as an O(tail) bounded read, so the cost scales
|
|
42
|
+
// with this floor, not with total accumulated history.
|
|
43
|
+
const HOT_TAIL_MIN_LIMIT = 60;
|
|
26
44
|
const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
|
|
45
|
+
// (A2.2) CLI_NATIVE_HISTORY_FRESH_MS removed with isNativeHistoryFreshEnough.
|
|
46
|
+
// Hardcoded native-transcript provider allow-list. Deprecated. Kept only as a
|
|
47
|
+
// last-resort fallback when ProviderModule is not yet loaded; on every hit we
|
|
48
|
+
// warn so the dependency on this set is visible. A2 deletes the set entirely
|
|
49
|
+
// and routes solely through canonicalHistory.contractVersion +
|
|
50
|
+
// isNativeSourceCanonicalHistory().
|
|
51
|
+
const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli', 'antigravity-cli']);
|
|
52
|
+
const warnedLegacyNativeAllowlistHits = new Set<string>();
|
|
53
|
+
function warnLegacyNativeAllowlistHit(providerType: string): void {
|
|
54
|
+
if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
|
|
55
|
+
warnedLegacyNativeAllowlistHits.add(providerType);
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.warn(
|
|
58
|
+
`[chat-commands] supportsCliNativeTranscript fell back to the hardcoded `
|
|
59
|
+
+ `CLI_NATIVE_TRANSCRIPT_PROVIDERS set for "${providerType}". `
|
|
60
|
+
+ `The provider module was unavailable or did not declare canonicalHistory. `
|
|
61
|
+
+ `Set canonicalHistory.contractVersion in the provider.json to remove this dependency.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
27
64
|
const recentSendByTarget = new Map<string, number>();
|
|
28
65
|
|
|
29
66
|
interface ApprovalSelectableInstance extends ProviderInstance {
|
|
@@ -32,6 +69,7 @@ interface ApprovalSelectableInstance extends ProviderInstance {
|
|
|
32
69
|
|
|
33
70
|
interface RuntimeChatMessageMerger extends ProviderInstance {
|
|
34
71
|
mergeRuntimeChatMessages?(messages: ChatMessage[]): ChatMessage[];
|
|
72
|
+
recordAcknowledgedUserInput?(input: InputEnvelope | string): void;
|
|
35
73
|
}
|
|
36
74
|
|
|
37
75
|
type LegacyStringScript = (params?: Record<string, unknown> | string) => string;
|
|
@@ -48,6 +86,16 @@ function getTargetedCliAdapter(h: CommandHelpers, args: any, providerType?: stri
|
|
|
48
86
|
return h.getCliAdapter(args?.targetSessionId || providerType || h.currentSession?.providerType || h.currentManagerKey);
|
|
49
87
|
}
|
|
50
88
|
|
|
89
|
+
function getExplicitHistorySessionId(args: any): string | undefined {
|
|
90
|
+
const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
|
|
91
|
+
if (explicit) return explicit;
|
|
92
|
+
|
|
93
|
+
const explicitProviderSessionId = typeof args?.providerSessionId === 'string' ? args.providerSessionId.trim() : '';
|
|
94
|
+
if (explicitProviderSessionId) return explicitProviderSessionId;
|
|
95
|
+
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
51
99
|
function getTargetInstance(h: CommandHelpers, args: any): ApprovalSelectableInstance | null {
|
|
52
100
|
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
53
101
|
const sessionId = targetSessionId || h.currentSession?.sessionId || '';
|
|
@@ -139,19 +187,82 @@ async function waitOnceForFreshHermesCliStart(adapter: CliAdapter, log: (msg: st
|
|
|
139
187
|
}
|
|
140
188
|
|
|
141
189
|
function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
|
|
142
|
-
const explicit =
|
|
190
|
+
const explicit = getExplicitHistorySessionId(args);
|
|
143
191
|
if (explicit) return explicit;
|
|
144
192
|
|
|
145
|
-
const explicitProviderSessionId = typeof args?.providerSessionId === 'string' ? args.providerSessionId.trim() : '';
|
|
146
|
-
if (explicitProviderSessionId) return explicitProviderSessionId;
|
|
147
|
-
|
|
148
193
|
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
149
194
|
if (!targetSessionId) return undefined;
|
|
150
195
|
|
|
151
|
-
const
|
|
196
|
+
const session = h.ctx.sessionRegistry?.get(targetSessionId) as any;
|
|
197
|
+
const registeredProviderSessionId = typeof session?.providerSessionId === 'string' ? session.providerSessionId.trim() : '';
|
|
198
|
+
if (registeredProviderSessionId) return registeredProviderSessionId;
|
|
199
|
+
|
|
200
|
+
const instance = getTargetInstance(h, args);
|
|
152
201
|
const state = instance?.getState?.();
|
|
153
202
|
const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
|
|
154
|
-
|
|
203
|
+
if (providerSessionId) return providerSessionId;
|
|
204
|
+
|
|
205
|
+
const currentSession = h.currentSession as any;
|
|
206
|
+
if (currentSession?.sessionId === targetSessionId) {
|
|
207
|
+
const currentProviderSessionId = typeof currentSession.providerSessionId === 'string'
|
|
208
|
+
? currentSession.providerSessionId.trim()
|
|
209
|
+
: '';
|
|
210
|
+
if (currentProviderSessionId) return currentProviderSessionId;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return targetSessionId;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveCliNativeHistorySessionId(args: any, currentHistorySessionId: string | undefined, parsedProviderSessionId: string | undefined): string | undefined {
|
|
217
|
+
const explicit = getExplicitHistorySessionId(args);
|
|
218
|
+
if (explicit) return explicit;
|
|
219
|
+
|
|
220
|
+
const parsed = typeof parsedProviderSessionId === 'string' ? parsedProviderSessionId.trim() : '';
|
|
221
|
+
const current = typeof currentHistorySessionId === 'string' ? currentHistorySessionId.trim() : '';
|
|
222
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
223
|
+
|
|
224
|
+
// getHistorySessionId falls back to the runtime session id when no native
|
|
225
|
+
// handle has been registered yet. For live CLI adapters the parser may
|
|
226
|
+
// already know the provider-native handle; prefer it over the runtime id so
|
|
227
|
+
// exact native reads do not miss the worker transcript and fall back to PTY
|
|
228
|
+
// or same-workspace history.
|
|
229
|
+
if (parsed && (!current || current === targetSessionId)) return parsed;
|
|
230
|
+
return current || parsed || undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function shouldSkipLiveCliNativeHistoryWithoutProviderSession(args: {
|
|
234
|
+
adapter?: CliAdapter | null;
|
|
235
|
+
providerType?: string;
|
|
236
|
+
readChatArgs: any;
|
|
237
|
+
nativeHistorySessionId?: string;
|
|
238
|
+
parsedProviderSessionId?: string;
|
|
239
|
+
}): boolean {
|
|
240
|
+
const explicit = getExplicitHistorySessionId(args.readChatArgs);
|
|
241
|
+
if (explicit) return false;
|
|
242
|
+
|
|
243
|
+
const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
|
|
244
|
+
? args.readChatArgs.targetSessionId.trim()
|
|
245
|
+
: '';
|
|
246
|
+
if (!targetSessionId) return false;
|
|
247
|
+
|
|
248
|
+
const resolved = typeof args.nativeHistorySessionId === 'string'
|
|
249
|
+
? args.nativeHistorySessionId.trim()
|
|
250
|
+
: '';
|
|
251
|
+
if (!resolved || resolved !== targetSessionId) return false;
|
|
252
|
+
|
|
253
|
+
const parsed = typeof args.parsedProviderSessionId === 'string'
|
|
254
|
+
? args.parsedProviderSessionId.trim()
|
|
255
|
+
: '';
|
|
256
|
+
if (parsed) return false;
|
|
257
|
+
|
|
258
|
+
const cliType = args.adapter?.cliType || args.providerType || '';
|
|
259
|
+
if (cliType !== 'codex-cli') return false;
|
|
260
|
+
|
|
261
|
+
// A live Codex session starts with only the daemon runtime UUID. That UUID
|
|
262
|
+
// is not the provider-native rollout id, so using it for native history
|
|
263
|
+
// lets the file picker fall back to the newest same-workspace transcript
|
|
264
|
+
// and makes concurrent fresh sessions all show the same old conversation.
|
|
265
|
+
return !!args.adapter;
|
|
155
266
|
}
|
|
156
267
|
|
|
157
268
|
function getInteractionId(args: any): string | undefined {
|
|
@@ -221,6 +332,1014 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
|
|
|
221
332
|
return normalizeChatMessages(messages);
|
|
222
333
|
}
|
|
223
334
|
|
|
335
|
+
function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
|
|
336
|
+
let newest = 0;
|
|
337
|
+
for (const message of messages) {
|
|
338
|
+
const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
|
|
339
|
+
if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
|
|
340
|
+
}
|
|
341
|
+
return newest;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function readHistorySessionIdFromMessages(messages: ChatMessage[]): string | undefined {
|
|
345
|
+
for (const message of messages as Array<ChatMessage & { historySessionId?: unknown }>) {
|
|
346
|
+
const historySessionId = typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '';
|
|
347
|
+
if (historySessionId) return historySessionId;
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function shouldPreserveNativeIdentity(providerType: string, sessionId: string, message: ChatMessage): boolean {
|
|
353
|
+
const providerUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
|
|
354
|
+
const turnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
|
|
355
|
+
if (!providerUnitKey) return false;
|
|
356
|
+
// (A2.3) v2 stamped identity is producer-owned and globally stable; trust it
|
|
357
|
+
// unconditionally. Producers may omit _turnKey (the daemon recomputes it
|
|
358
|
+
// from the current ordering), so do not require turnKey for v2 messages.
|
|
359
|
+
if (providerUnitKey.startsWith('v2:') || providerUnitKey.startsWith('v2-pty:')) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
// v1 identity always required both keys to be present.
|
|
363
|
+
if (!turnKey) return false;
|
|
364
|
+
if (providerType === 'hermes-cli' && sessionId) {
|
|
365
|
+
return providerUnitKey.startsWith(`${providerType}:native:${sessionId}:`)
|
|
366
|
+
&& turnKey.startsWith(`${providerType}:native-turn:${sessionId}:`);
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Drop the synthetic "user" message some CLIs surface in their native
|
|
373
|
+
* transcript when the daemon injects a coordinator system prompt
|
|
374
|
+
* (codex puts the AGENTS.md / developer_instructions block in as
|
|
375
|
+
* role=user; agy/claude/hermes have similar artifacts). The user can
|
|
376
|
+
* opt back into seeing it via the provider setting
|
|
377
|
+
* `showCoordinatorSystemPrompt`. Default is off — the prompt is still
|
|
378
|
+
* fully visible from the chat-header ⓘ "Session info" dialog.
|
|
379
|
+
*
|
|
380
|
+
* Matching rules:
|
|
381
|
+
* 1. Setting must be off (default).
|
|
382
|
+
* 2. There must be a registered coordinator entry for the session.
|
|
383
|
+
* 3. The candidate message is filtered when its role is user OR
|
|
384
|
+
* system and its content either contains the prompt body verbatim,
|
|
385
|
+
* OR contains the well-known coordinator marker
|
|
386
|
+
* `adhdev-mesh-coordinator-prompt`. The marker covers context-file
|
|
387
|
+
* cases (agy AGENTS.md / gemini GEMINI.md) where the CLI may wrap
|
|
388
|
+
* its own preamble around our block. Verbatim-content covers
|
|
389
|
+
* codex's developer_instructions echo.
|
|
390
|
+
*
|
|
391
|
+
* Returns the messages array unchanged when none of the rules match,
|
|
392
|
+
* so this is safe to apply unconditionally to every read_chat result.
|
|
393
|
+
*/
|
|
394
|
+
function maybeHideCoordinatorPromptMessage(
|
|
395
|
+
h: CommandHelpers,
|
|
396
|
+
providerType: string,
|
|
397
|
+
sessionId: string | undefined,
|
|
398
|
+
messages: ChatMessage[],
|
|
399
|
+
): ChatMessage[] {
|
|
400
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
401
|
+
if (!sessionId) return messages;
|
|
402
|
+
const loader = h.ctx?.providerLoader;
|
|
403
|
+
if (!loader) return messages;
|
|
404
|
+
let showSetting: unknown = undefined;
|
|
405
|
+
try {
|
|
406
|
+
showSetting = (loader as any).getSettingValue?.(providerType, 'showCoordinatorSystemPrompt');
|
|
407
|
+
} catch { /* unknown setting key for this provider — fall through */ }
|
|
408
|
+
if (showSetting === true) return messages;
|
|
409
|
+
const coord = getCoordinatorForSession(sessionId);
|
|
410
|
+
if (!coord) return messages;
|
|
411
|
+
const promptBody = typeof coord.systemPrompt === 'string' ? coord.systemPrompt : '';
|
|
412
|
+
const MARKER = 'adhdev-mesh-coordinator-prompt';
|
|
413
|
+
const filtered = messages.filter(m => {
|
|
414
|
+
const role = String((m as any)?.role || '').toLowerCase();
|
|
415
|
+
if (role !== 'user' && role !== 'system') return true;
|
|
416
|
+
const content = flattenContent((m as any)?.content);
|
|
417
|
+
if (!content) return true;
|
|
418
|
+
if (content.includes(MARKER)) return false;
|
|
419
|
+
if (promptBody && content.includes(promptBody.slice(0, Math.min(400, promptBody.length)))) return false;
|
|
420
|
+
return true;
|
|
421
|
+
});
|
|
422
|
+
if (filtered.length !== messages.length) {
|
|
423
|
+
LOG.debug('ChatFilter', `[${providerType}] hid ${messages.length - filtered.length} coordinator-prompt message(s) from ${sessionId}`);
|
|
424
|
+
}
|
|
425
|
+
return filtered;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Convenience wrapper used at every native-history call site: normalize +
|
|
430
|
+
* conditionally drop the coordinator system-prompt message. Avoids
|
|
431
|
+
* duplicating the filter at four read_chat code paths.
|
|
432
|
+
*/
|
|
433
|
+
function normalizeAndFilterNativeHistory(
|
|
434
|
+
h: CommandHelpers,
|
|
435
|
+
providerType: string,
|
|
436
|
+
args: any,
|
|
437
|
+
messages: ChatMessage[],
|
|
438
|
+
nativeSessionId?: string,
|
|
439
|
+
): ChatMessage[] {
|
|
440
|
+
const normalized = normalizeNativeHistoryMessages(providerType, messages, nativeSessionId);
|
|
441
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId
|
|
442
|
+
: typeof args?.sessionId === 'string' ? args.sessionId
|
|
443
|
+
: undefined;
|
|
444
|
+
return maybeHideCoordinatorPromptMessage(h, providerType, sessionId, normalized);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function normalizeNativeHistoryMessages(providerType: string, messages: ChatMessage[], nativeSessionId?: string): ChatMessage[] {
|
|
448
|
+
let turnIndex = 0;
|
|
449
|
+
return normalizeChatMessages(messages).map((message, index) => {
|
|
450
|
+
const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
451
|
+
const kind = typeof message.kind === 'string' && message.kind.trim() ? message.kind.trim() : (role === 'system' ? 'system' : 'standard');
|
|
452
|
+
if ((role === 'user' || role === 'human') && index > 0) turnIndex += 1;
|
|
453
|
+
const historySessionId = typeof (message as any).historySessionId === 'string'
|
|
454
|
+
? (message as any).historySessionId.trim()
|
|
455
|
+
: '';
|
|
456
|
+
const contentHash = hashSignatureParts([
|
|
457
|
+
providerType,
|
|
458
|
+
historySessionId,
|
|
459
|
+
String(message.receivedAt || message.timestamp || index),
|
|
460
|
+
role,
|
|
461
|
+
kind,
|
|
462
|
+
flattenContent(message.content),
|
|
463
|
+
]).slice(0, 12);
|
|
464
|
+
const nativeIdentitySessionId = historySessionId || (typeof nativeSessionId === 'string' ? nativeSessionId.trim() : '');
|
|
465
|
+
const preserveNativeIdentity = shouldPreserveNativeIdentity(providerType, nativeIdentitySessionId, message);
|
|
466
|
+
const existingProviderUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
|
|
467
|
+
const existingTurnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
|
|
468
|
+
const providerUnitKey = preserveNativeIdentity
|
|
469
|
+
? existingProviderUnitKey
|
|
470
|
+
: `${providerType}:native:${nativeIdentitySessionId || 'workspace'}:${index}:${role || 'message'}:${kind}:${contentHash}`;
|
|
471
|
+
const meta = message.meta && typeof message.meta === 'object' ? message.meta as Record<string, unknown> : undefined;
|
|
472
|
+
const isSystemSessionStart = role === 'system' || kind === 'system' || kind === 'session_start';
|
|
473
|
+
const isActivity = role === 'assistant' && (kind === 'tool' || kind === 'terminal' || kind === 'thought');
|
|
474
|
+
// (A2.3) sequence emit. Producer-supplied wins (v2-stamped messages
|
|
475
|
+
// bring their own monotonic sequence); otherwise derive from
|
|
476
|
+
// receivedAt/timestamp; otherwise positional. Always present on the
|
|
477
|
+
// output so consumers (ChatSourceMachine) have a stable ordering key.
|
|
478
|
+
const existingSequence = typeof (message as any).sequence === 'number'
|
|
479
|
+
&& Number.isFinite((message as any).sequence)
|
|
480
|
+
? (message as any).sequence
|
|
481
|
+
: null;
|
|
482
|
+
const tsCandidate = Number(message.receivedAt || message.timestamp || 0);
|
|
483
|
+
const sequence = existingSequence !== null
|
|
484
|
+
? existingSequence
|
|
485
|
+
: (tsCandidate > 0 ? tsCandidate : index);
|
|
486
|
+
return {
|
|
487
|
+
...message,
|
|
488
|
+
role: role === 'human' ? 'user' : (role || 'assistant'),
|
|
489
|
+
kind: isSystemSessionStart ? 'system' : kind,
|
|
490
|
+
...(nativeIdentitySessionId ? { historySessionId: nativeIdentitySessionId } : {}),
|
|
491
|
+
providerUnitKey,
|
|
492
|
+
bubbleId: typeof message.bubbleId === 'string' && message.bubbleId.trim()
|
|
493
|
+
&& preserveNativeIdentity
|
|
494
|
+
? message.bubbleId.trim()
|
|
495
|
+
: `bubble:${providerUnitKey}`,
|
|
496
|
+
sequence,
|
|
497
|
+
_turnKey: preserveNativeIdentity
|
|
498
|
+
? existingTurnKey
|
|
499
|
+
: `${providerType}:native-turn:${nativeIdentitySessionId || 'workspace'}:${turnIndex}`,
|
|
500
|
+
bubbleState: message.bubbleState || 'final',
|
|
501
|
+
...(isSystemSessionStart ? {
|
|
502
|
+
visibility: message.visibility || 'hidden',
|
|
503
|
+
transcriptVisibility: message.transcriptVisibility || 'hidden',
|
|
504
|
+
audience: message.audience || 'internal',
|
|
505
|
+
source: message.source || 'runtime_status',
|
|
506
|
+
} : isActivity ? {
|
|
507
|
+
source: message.source || (kind === 'terminal' ? 'terminal_command' : 'tool_call'),
|
|
508
|
+
meta: { ...meta, label: message.senderName || meta?.label || (kind === 'terminal' ? 'Terminal' : 'Tool') },
|
|
509
|
+
} : {
|
|
510
|
+
source: message.source || (role === 'assistant' ? 'assistant_text' : undefined),
|
|
511
|
+
}),
|
|
512
|
+
} as ChatMessage;
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function buildCliMessageSourceProvenance(args: {
|
|
517
|
+
selected: 'native-history' | 'pty-parser';
|
|
518
|
+
provider: string;
|
|
519
|
+
nativeHandle?: string;
|
|
520
|
+
sessionWorkspace?: string;
|
|
521
|
+
intendedWorkspace?: string;
|
|
522
|
+
transcriptWorkspace?: string;
|
|
523
|
+
fallbackReason?: string;
|
|
524
|
+
nativeSource?: string;
|
|
525
|
+
sourcePath?: string;
|
|
526
|
+
sourceMtimeMs?: number;
|
|
527
|
+
nativeHistoryCoverage?: string;
|
|
528
|
+
partialReason?: string;
|
|
529
|
+
unavailableReason?: string;
|
|
530
|
+
nativeMessages?: ChatMessage[];
|
|
531
|
+
ptyMessages?: ChatMessage[];
|
|
532
|
+
returnedMessages?: ChatMessage[];
|
|
533
|
+
safeMapping?: boolean;
|
|
534
|
+
freshEnough?: boolean;
|
|
535
|
+
ptyStatusApprovalOnly?: boolean;
|
|
536
|
+
}): Record<string, unknown> {
|
|
537
|
+
const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
|
|
538
|
+
const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
|
|
539
|
+
const nativeMessages = args.nativeMessages || [];
|
|
540
|
+
const ptyMessages = args.ptyMessages || [];
|
|
541
|
+
const returnedMessages = args.returnedMessages || [];
|
|
542
|
+
const identityStatus = args.selected === 'native-history'
|
|
543
|
+
? 'safe'
|
|
544
|
+
: args.fallbackReason === 'native_history_not_safely_mapped'
|
|
545
|
+
? 'ambiguous_session_identity'
|
|
546
|
+
: args.fallbackReason?.startsWith('native_history_unavailable')
|
|
547
|
+
? 'transcript_unmapped'
|
|
548
|
+
: undefined;
|
|
549
|
+
return {
|
|
550
|
+
selected: args.selected,
|
|
551
|
+
provider: args.provider,
|
|
552
|
+
providerType: args.provider,
|
|
553
|
+
...(identityStatus ? { identityStatus } : {}),
|
|
554
|
+
...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
|
|
555
|
+
...(args.nativeHandle ? { nativeSessionId: args.nativeHandle } : {}),
|
|
556
|
+
...(args.sessionWorkspace ? { sessionWorkspace: args.sessionWorkspace } : {}),
|
|
557
|
+
...(args.intendedWorkspace ? { intendedWorkspace: args.intendedWorkspace } : {}),
|
|
558
|
+
...(args.transcriptWorkspace ? { transcriptWorkspace: args.transcriptWorkspace } : {}),
|
|
559
|
+
...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
|
|
560
|
+
...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
|
|
561
|
+
...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
|
|
562
|
+
...(args.nativeHistoryCoverage ? { nativeHistoryCoverage: args.nativeHistoryCoverage } : {}),
|
|
563
|
+
...(args.partialReason ? { partialReason: args.partialReason } : {}),
|
|
564
|
+
...(args.unavailableReason ? { unavailableReason: args.unavailableReason } : {}),
|
|
565
|
+
ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
|
|
566
|
+
staleness: {
|
|
567
|
+
sourceMtimeMs: sourceMtimeMs || undefined,
|
|
568
|
+
sourceMtimeAgeMs,
|
|
569
|
+
nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
|
|
570
|
+
ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
|
|
571
|
+
freshEnough: args.freshEnough === true,
|
|
572
|
+
},
|
|
573
|
+
coverage: {
|
|
574
|
+
nativeMessageCount: nativeMessages.length,
|
|
575
|
+
ptyMessageCount: ptyMessages.length,
|
|
576
|
+
returnedMessageCount: returnedMessages.length,
|
|
577
|
+
safeMapping: args.safeMapping === true,
|
|
578
|
+
// true when PTY message bodies are suppressed and must not be treated as
|
|
579
|
+
// chat content. PTY may still contribute status/approval/screen evidence.
|
|
580
|
+
ptyMessagesSuppressed: args.selected === 'native-history' || args.ptyStatusApprovalOnly === true,
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Map a ChatSourceMachine transition cause back to the v1 messageSource
|
|
587
|
+
* `fallbackReason` vocabulary so legacy consumers (web-cloud, tests, mesh
|
|
588
|
+
* debug bundles) keep parsing strings they already know. A3 replaces the
|
|
589
|
+
* caller surface with stateTransition/lockState, after which this map can be
|
|
590
|
+
* deleted.
|
|
591
|
+
*
|
|
592
|
+
* Returns undefined when the cause does not correspond to a fallback (i.e.
|
|
593
|
+
* the source is native-history and there is nothing to explain).
|
|
594
|
+
*/
|
|
595
|
+
function causeToLegacyFallbackReason(
|
|
596
|
+
cause: ChatSourceTransitionCause,
|
|
597
|
+
selected: 'native-history' | 'pty-parser',
|
|
598
|
+
extraDetail?: { unavailableReason?: string; nativeSource?: string },
|
|
599
|
+
): string | undefined {
|
|
600
|
+
if (selected === 'native-history') return undefined;
|
|
601
|
+
switch (cause) {
|
|
602
|
+
case 'initial':
|
|
603
|
+
return 'native_history_not_checked';
|
|
604
|
+
case 'native_progressed':
|
|
605
|
+
// Selected pty-parser despite a progressed observation — that
|
|
606
|
+
// means we held PtyOnly stickily (peak unmet or non-superset).
|
|
607
|
+
return 'native_history_not_selected';
|
|
608
|
+
case 'native_regressed_shrunk':
|
|
609
|
+
return 'native_history_empty';
|
|
610
|
+
case 'native_regressed_unsafe_mapping':
|
|
611
|
+
return 'native_history_not_safely_mapped';
|
|
612
|
+
case 'native_regressed_coverage_partial':
|
|
613
|
+
return 'native_history_partial';
|
|
614
|
+
case 'native_regressed_coverage_unavailable':
|
|
615
|
+
return 'native_history_unavailable';
|
|
616
|
+
case 'native_unavailable_read_error':
|
|
617
|
+
return extraDetail?.unavailableReason
|
|
618
|
+
? `native_history_unavailable:${extraDetail.unavailableReason}`
|
|
619
|
+
: 'native_history_unavailable';
|
|
620
|
+
case 'native_unavailable_provider_unsupported':
|
|
621
|
+
return 'provider_native_transcript_not_supported';
|
|
622
|
+
case 'native_unavailable_empty':
|
|
623
|
+
return 'native_history_empty';
|
|
624
|
+
case 'native_unavailable_not_native_source':
|
|
625
|
+
return extraDetail?.nativeSource
|
|
626
|
+
? `native_history_source_${extraDetail.nativeSource}`
|
|
627
|
+
: 'native_history_unavailable';
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Translate a native-history fetch result + provider/adapter context into a
|
|
633
|
+
* ChatSourceObservation and drive ChatSourceRegistry. Returns the decision
|
|
634
|
+
* together with the legacy messageSource payload so call sites can produce
|
|
635
|
+
* a v1-compatible response without duplicating the registry plumbing.
|
|
636
|
+
*
|
|
637
|
+
* This is the replacement for the 300-line if-ladder that previously lived
|
|
638
|
+
* inline in handleReadChat. It is intentionally split out for two reasons:
|
|
639
|
+
* (1) we will call it from two places (CLI adapter branch + history-only
|
|
640
|
+
* branch) instead of duplicating the ladder, (2) tests can drive it with
|
|
641
|
+
* synthetic native-history results to verify the cause→fallbackReason
|
|
642
|
+
* mapping without booting the whole readChat pipeline.
|
|
643
|
+
*/
|
|
644
|
+
function decideCliReadChatSource(args: {
|
|
645
|
+
providerType: string;
|
|
646
|
+
provider?: ProviderModule;
|
|
647
|
+
sessionId: string;
|
|
648
|
+
nativeHistoryResult: any | null;
|
|
649
|
+
nativeHistoryError?: unknown;
|
|
650
|
+
safeMapping: boolean;
|
|
651
|
+
trustedExactNativeIdentity?: boolean;
|
|
652
|
+
sessionWorkspace?: string;
|
|
653
|
+
intendedWorkspace?: string;
|
|
654
|
+
ptyMessages: ChatMessage[];
|
|
655
|
+
ptyStatusApprovalOnly: boolean;
|
|
656
|
+
}): {
|
|
657
|
+
decision: ChatSourceDecision;
|
|
658
|
+
messageSource: Record<string, unknown>;
|
|
659
|
+
nativeMessages: ChatMessage[];
|
|
660
|
+
nativeSelected: boolean;
|
|
661
|
+
} {
|
|
662
|
+
const supportsNative = supportsCliNativeTranscript(args.providerType, args.provider);
|
|
663
|
+
const observation = buildObservationForCli(args, supportsNative);
|
|
664
|
+
const sessionKey = chatSourceSessionKey(args.providerType, args.sessionId);
|
|
665
|
+
let decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
|
|
666
|
+
|
|
667
|
+
// A restored runtime can briefly expose different native slices while the
|
|
668
|
+
// provider transcript settles (for example, startup/system rows may be
|
|
669
|
+
// filtered after the first read). The source machine correctly treats a
|
|
670
|
+
// shrinking slice as regression, but an exact provider-session lookup with
|
|
671
|
+
// no PTY transcript has no safer fallback. Re-bootstrap only this proven
|
|
672
|
+
// identity so the chat does not disappear after daemon restart.
|
|
673
|
+
if (
|
|
674
|
+
decision.selected === 'pty-parser'
|
|
675
|
+
&& args.trustedExactNativeIdentity === true
|
|
676
|
+
&& args.safeMapping
|
|
677
|
+
&& args.ptyMessages.length === 0
|
|
678
|
+
&& observation.kind === 'native_present'
|
|
679
|
+
&& observation.coverage !== 'partial'
|
|
680
|
+
&& observation.messages.length > 0
|
|
681
|
+
) {
|
|
682
|
+
CHAT_SOURCE_REGISTRY.clear(sessionKey);
|
|
683
|
+
decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const nativeMessages: ChatMessage[] = observation.kind === 'native_present'
|
|
687
|
+
? extractNativeMessagesFromResult(args.providerType, args.nativeHistoryResult)
|
|
688
|
+
: [];
|
|
689
|
+
|
|
690
|
+
const nativeSource = typeof args.nativeHistoryResult?.source === 'string'
|
|
691
|
+
? args.nativeHistoryResult.source
|
|
692
|
+
: undefined;
|
|
693
|
+
const sourcePath = typeof args.nativeHistoryResult?.sourcePath === 'string'
|
|
694
|
+
? args.nativeHistoryResult.sourcePath
|
|
695
|
+
: undefined;
|
|
696
|
+
const sourceMtimeMs = typeof args.nativeHistoryResult?.sourceMtimeMs === 'number'
|
|
697
|
+
? args.nativeHistoryResult.sourceMtimeMs
|
|
698
|
+
: undefined;
|
|
699
|
+
const coverageHint = typeof args.nativeHistoryResult?.nativeHistoryCoverage === 'string'
|
|
700
|
+
? args.nativeHistoryResult.nativeHistoryCoverage
|
|
701
|
+
: undefined;
|
|
702
|
+
const partialReason = typeof args.nativeHistoryResult?.partialReason === 'string'
|
|
703
|
+
? args.nativeHistoryResult.partialReason
|
|
704
|
+
: undefined;
|
|
705
|
+
const unavailableReason = typeof args.nativeHistoryResult?.unavailableReason === 'string'
|
|
706
|
+
? args.nativeHistoryResult.unavailableReason
|
|
707
|
+
: args.nativeHistoryError
|
|
708
|
+
? `error:${(args.nativeHistoryError as any)?.message || String(args.nativeHistoryError)}`
|
|
709
|
+
: undefined;
|
|
710
|
+
const nativeHandle = typeof args.nativeHistoryResult?.providerSessionId === 'string'
|
|
711
|
+
? args.nativeHistoryResult.providerSessionId
|
|
712
|
+
: undefined;
|
|
713
|
+
const transcriptWorkspace = typeof args.nativeHistoryResult?.workspace === 'string'
|
|
714
|
+
? args.nativeHistoryResult.workspace
|
|
715
|
+
: nativeMessages.map((m: any) => typeof m?.workspace === 'string' ? m.workspace.trim() : '').find(Boolean);
|
|
716
|
+
|
|
717
|
+
const fallbackReason = causeToLegacyFallbackReason(decision.transition.cause, decision.selected, {
|
|
718
|
+
unavailableReason,
|
|
719
|
+
nativeSource: nativeSource && nativeSource !== 'provider-native' ? nativeSource : undefined,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// ptyStatusApprovalOnly: when the machine selected native-history we
|
|
723
|
+
// suppress PTY content so the dashboard does not double-show messages
|
|
724
|
+
// already in the native transcript. When the machine selected
|
|
725
|
+
// pty-parser, PTY is the authoritative source — do NOT suppress it.
|
|
726
|
+
// Callers used to hard-code this to `nativeSelected first = true` which
|
|
727
|
+
// suppressed PTY content even when native was empty/unavailable, leaving
|
|
728
|
+
// the dashboard with zero visible messages (the codex generating/waiting
|
|
729
|
+
// approval stuck state). Trust the machine here, not the caller hint.
|
|
730
|
+
const ptyStatusApprovalOnly = decision.selected === 'native-history'
|
|
731
|
+
? true
|
|
732
|
+
: args.ptyStatusApprovalOnly;
|
|
733
|
+
|
|
734
|
+
const messageSource = buildCliMessageSourceProvenance({
|
|
735
|
+
selected: decision.selected,
|
|
736
|
+
provider: args.providerType,
|
|
737
|
+
nativeHandle,
|
|
738
|
+
sessionWorkspace: args.sessionWorkspace,
|
|
739
|
+
intendedWorkspace: args.intendedWorkspace,
|
|
740
|
+
transcriptWorkspace,
|
|
741
|
+
fallbackReason,
|
|
742
|
+
nativeSource,
|
|
743
|
+
sourcePath,
|
|
744
|
+
sourceMtimeMs,
|
|
745
|
+
nativeHistoryCoverage: coverageHint,
|
|
746
|
+
partialReason,
|
|
747
|
+
unavailableReason,
|
|
748
|
+
nativeMessages,
|
|
749
|
+
ptyMessages: args.ptyMessages,
|
|
750
|
+
returnedMessages: decision.selected === 'native-history' ? nativeMessages : args.ptyMessages,
|
|
751
|
+
safeMapping: args.safeMapping,
|
|
752
|
+
// freshEnough is a v1 concept the machine does not model directly.
|
|
753
|
+
// We surface lockState.locked here so v1 consumers reading
|
|
754
|
+
// staleness.freshEnough still get a meaningful boolean.
|
|
755
|
+
freshEnough: decision.lockState.locked,
|
|
756
|
+
ptyStatusApprovalOnly,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
decision,
|
|
761
|
+
messageSource,
|
|
762
|
+
nativeMessages,
|
|
763
|
+
nativeSelected: decision.selected === 'native-history',
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function buildObservationForCli(
|
|
768
|
+
args: {
|
|
769
|
+
providerType: string;
|
|
770
|
+
sessionId: string;
|
|
771
|
+
nativeHistoryResult: any | null;
|
|
772
|
+
nativeHistoryError?: unknown;
|
|
773
|
+
safeMapping: boolean;
|
|
774
|
+
},
|
|
775
|
+
supportsNative: boolean,
|
|
776
|
+
): ChatSourceObservation {
|
|
777
|
+
if (!supportsNative) {
|
|
778
|
+
return { kind: 'native_unavailable', reason: 'provider_not_supported' };
|
|
779
|
+
}
|
|
780
|
+
if (args.nativeHistoryError) {
|
|
781
|
+
return { kind: 'native_unavailable', reason: 'read_error' };
|
|
782
|
+
}
|
|
783
|
+
const result = args.nativeHistoryResult;
|
|
784
|
+
if (!result || typeof result !== 'object') {
|
|
785
|
+
return { kind: 'native_unavailable', reason: 'read_error' };
|
|
786
|
+
}
|
|
787
|
+
const source = typeof result.source === 'string' ? result.source : '';
|
|
788
|
+
if (source && source !== 'provider-native') {
|
|
789
|
+
// 'native-unavailable' or other producer-side declined source.
|
|
790
|
+
return { kind: 'native_unavailable', reason: source === 'native-unavailable' ? 'empty' : 'not_native_source' };
|
|
791
|
+
}
|
|
792
|
+
const messages = Array.isArray(result.messages) ? result.messages : [];
|
|
793
|
+
if (messages.length === 0) {
|
|
794
|
+
return { kind: 'native_unavailable', reason: 'empty' };
|
|
795
|
+
}
|
|
796
|
+
const coverage = typeof result.nativeHistoryCoverage === 'string'
|
|
797
|
+
? result.nativeHistoryCoverage
|
|
798
|
+
: 'tail';
|
|
799
|
+
if (coverage === 'unavailable') {
|
|
800
|
+
return { kind: 'native_unavailable', reason: 'coverage_unavailable' };
|
|
801
|
+
}
|
|
802
|
+
return buildV1NativePresentObservation({
|
|
803
|
+
providerType: args.providerType,
|
|
804
|
+
sessionId: args.sessionId,
|
|
805
|
+
messages,
|
|
806
|
+
coverage: coverage === 'full' || coverage === 'tail' || coverage === 'current-turn' || coverage === 'partial'
|
|
807
|
+
? coverage
|
|
808
|
+
: 'tail',
|
|
809
|
+
safeMapping: args.safeMapping,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function extractNativeMessagesFromResult(providerType: string, result: any): ChatMessage[] {
|
|
814
|
+
if (!result || !Array.isArray(result.messages)) return [];
|
|
815
|
+
return normalizeNativeHistoryMessages(
|
|
816
|
+
providerType,
|
|
817
|
+
result.messages as ChatMessage[],
|
|
818
|
+
typeof result.providerSessionId === 'string' ? result.providerSessionId : undefined,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* ptyStatusApprovalOnly is true when the daemon should treat PTY content as
|
|
824
|
+
* status/approval signal only (not as chat messages). v1 set this to `true`
|
|
825
|
+
* whenever native-history was selected as the source, and `false` otherwise.
|
|
826
|
+
* The machine equivalent: when native is the source we want PTY suppressed.
|
|
827
|
+
*/
|
|
828
|
+
function primaryPtyApprovalOnlyFor(_cliType: string, nativeSelected: boolean): boolean {
|
|
829
|
+
return nativeSelected;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Codex-only unsafe-native fallback: when the primary native fetch produced
|
|
834
|
+
* unsafe-mapping data, v1 attempted to recover by reading exact runtime
|
|
835
|
+
* mirror messages, runtime input ACK messages, or by trusting the current-
|
|
836
|
+
* runtime PTY when safely attributed. None of this is the machine's
|
|
837
|
+
* responsibility — the machine already decided pty-parser. This helper
|
|
838
|
+
* preserves the daemon-side message selection and annotates messageSource.
|
|
839
|
+
*/
|
|
840
|
+
function applyUnsafeNativeDaemonFallback(args: {
|
|
841
|
+
providerType: string;
|
|
842
|
+
adapter: CliAdapter;
|
|
843
|
+
helpers: CommandHelpers;
|
|
844
|
+
readChatArgs: any;
|
|
845
|
+
sessionWorkspace?: string;
|
|
846
|
+
intendedWorkspace?: string;
|
|
847
|
+
ptyMessages: ChatMessage[];
|
|
848
|
+
nativeHistoryLimit: number;
|
|
849
|
+
provider?: ProviderModule;
|
|
850
|
+
messageSourceRef: { set(value: Record<string, unknown>): void; get(): Record<string, unknown> };
|
|
851
|
+
apply(selection: {
|
|
852
|
+
messages: ChatMessage[];
|
|
853
|
+
transcriptAuthority?: 'provider' | 'daemon';
|
|
854
|
+
coverage?: 'full' | 'tail' | 'current-turn';
|
|
855
|
+
status?: string;
|
|
856
|
+
}): void;
|
|
857
|
+
activeModal: unknown;
|
|
858
|
+
returnedStatus: string;
|
|
859
|
+
coverage?: 'full' | 'tail' | 'current-turn';
|
|
860
|
+
}): void {
|
|
861
|
+
if (args.adapter.cliType !== 'codex-cli') {
|
|
862
|
+
// Only codex-cli had v1 daemon mirror recovery. Other providers skip.
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const ms = args.messageSourceRef.get();
|
|
866
|
+
const fallbackReason = typeof ms.fallbackReason === 'string' ? ms.fallbackReason : '';
|
|
867
|
+
if (!isUnsafeNativeTranscriptFallback(fallbackReason)) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const safeCurrentRuntimePtyMessages = isCurrentRuntimePtySafelyAttributed({
|
|
871
|
+
adapter: args.adapter,
|
|
872
|
+
helpers: args.helpers,
|
|
873
|
+
readChatArgs: args.readChatArgs,
|
|
874
|
+
sessionWorkspace: args.sessionWorkspace,
|
|
875
|
+
intendedWorkspace: args.intendedWorkspace,
|
|
876
|
+
ptyMessages: args.ptyMessages,
|
|
877
|
+
});
|
|
878
|
+
if (safeCurrentRuntimePtyMessages) {
|
|
879
|
+
args.apply({
|
|
880
|
+
messages: args.ptyMessages,
|
|
881
|
+
transcriptAuthority: 'daemon',
|
|
882
|
+
coverage: args.coverage || 'current-turn',
|
|
883
|
+
status: args.returnedStatus,
|
|
884
|
+
});
|
|
885
|
+
const next = { ...ms, selectedDaemonSource: 'current-runtime-pty', transcriptAuthority: 'daemon', runtimeMappingSafe: true };
|
|
886
|
+
args.messageSourceRef.set(next);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const safeRuntimeAckMessages = selectRuntimeInputAckMessages(args.ptyMessages);
|
|
890
|
+
if (safeRuntimeAckMessages.length > 0) {
|
|
891
|
+
args.apply({
|
|
892
|
+
messages: safeRuntimeAckMessages,
|
|
893
|
+
transcriptAuthority: 'daemon',
|
|
894
|
+
coverage: 'tail',
|
|
895
|
+
status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
|
|
896
|
+
});
|
|
897
|
+
const next = { ...ms, ptyStatusApprovalOnly: true };
|
|
898
|
+
args.messageSourceRef.set(next);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const exactRuntimeMirrorMessages = readExactRuntimeMirrorMessages({
|
|
902
|
+
providerType: args.providerType,
|
|
903
|
+
targetSessionId: typeof args.readChatArgs?.targetSessionId === 'string' ? args.readChatArgs.targetSessionId : undefined,
|
|
904
|
+
currentSessionId: typeof (args.helpers.currentSession as any)?.sessionId === 'string' ? (args.helpers.currentSession as any).sessionId : undefined,
|
|
905
|
+
tailLimit: args.nativeHistoryLimit,
|
|
906
|
+
historyBehavior: args.provider?.historyBehavior,
|
|
907
|
+
});
|
|
908
|
+
if (exactRuntimeMirrorMessages.length > 0) {
|
|
909
|
+
args.apply({
|
|
910
|
+
messages: exactRuntimeMirrorMessages,
|
|
911
|
+
transcriptAuthority: 'daemon',
|
|
912
|
+
coverage: 'tail',
|
|
913
|
+
status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
|
|
914
|
+
});
|
|
915
|
+
const next = { ...ms, selectedDaemonSource: 'exact-runtime-mirror', transcriptAuthority: 'daemon', ptyStatusApprovalOnly: true };
|
|
916
|
+
args.messageSourceRef.set(next);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
// No daemon mirror available — keep PTY messages as-is (still pty-parser
|
|
920
|
+
// selection); just coerce status for waiting_approval consistency.
|
|
921
|
+
args.apply({
|
|
922
|
+
messages: args.ptyMessages,
|
|
923
|
+
coverage: args.coverage,
|
|
924
|
+
status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
|
|
925
|
+
});
|
|
926
|
+
const next = { ...ms, ptyStatusApprovalOnly: true };
|
|
927
|
+
args.messageSourceRef.set(next);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// (A2.2) buildNativeHistoryFallbackReason removed. ChatSourceMachine emits a
|
|
931
|
+
// ChatSourceTransitionCause; causeToLegacyFallbackReason maps it back to the
|
|
932
|
+
// v1 vocabulary for response compatibility. A3 deletes the v1 vocabulary
|
|
933
|
+
// entirely and surfaces stateTransition/lockState directly.
|
|
934
|
+
|
|
935
|
+
function isUnsafeNativeTranscriptFallback(reason?: string): boolean {
|
|
936
|
+
const value = String(reason || '').trim();
|
|
937
|
+
return value.startsWith('native_history_unavailable')
|
|
938
|
+
|| value === 'native_history_not_safely_mapped'
|
|
939
|
+
|| value === 'native_history_stale'
|
|
940
|
+
|| value === 'native_history_partial';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function coerceUnsafeNativeFallbackStatus(status: string, activeModal: unknown): string {
|
|
944
|
+
if (status === 'waiting_approval' && activeModal) return status;
|
|
945
|
+
return 'idle';
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function isRuntimeInputAckMessage(message: ChatMessage | undefined): boolean {
|
|
949
|
+
if (!message || typeof message !== 'object') return false;
|
|
950
|
+
const role = String((message as any).role || '').trim().toLowerCase();
|
|
951
|
+
if (role !== 'user' && role !== 'human') return false;
|
|
952
|
+
const meta = (message as any).meta;
|
|
953
|
+
return !!meta && typeof meta === 'object' && !Array.isArray(meta) && meta.runtimeInputAck === true;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function selectRuntimeInputAckMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
957
|
+
return messages.filter((message) => isRuntimeInputAckMessage(message));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function readExactRuntimeMirrorMessages(args: {
|
|
961
|
+
providerType: string;
|
|
962
|
+
targetSessionId?: string;
|
|
963
|
+
currentSessionId?: string;
|
|
964
|
+
tailLimit: number;
|
|
965
|
+
historyBehavior?: ProviderModule['historyBehavior'];
|
|
966
|
+
}): ChatMessage[] {
|
|
967
|
+
const targetSessionId = String(args.targetSessionId || '').trim();
|
|
968
|
+
const currentSessionId = String(args.currentSessionId || '').trim();
|
|
969
|
+
if (!targetSessionId || targetSessionId !== currentSessionId) return [];
|
|
970
|
+
|
|
971
|
+
const history = readChatHistory(
|
|
972
|
+
args.providerType,
|
|
973
|
+
0,
|
|
974
|
+
Math.max(args.tailLimit || 0, HOT_TAIL_MIN_LIMIT),
|
|
975
|
+
targetSessionId,
|
|
976
|
+
0,
|
|
977
|
+
args.historyBehavior,
|
|
978
|
+
);
|
|
979
|
+
return normalizeChatMessages((history.messages || []) as ChatMessage[])
|
|
980
|
+
.filter((message) => {
|
|
981
|
+
const historySessionId = String((message as any).historySessionId || '').trim();
|
|
982
|
+
const instanceId = String((message as any).instanceId || '').trim();
|
|
983
|
+
return historySessionId === targetSessionId || instanceId === targetSessionId;
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function normalizeComparableWorkspace(value: unknown): string {
|
|
988
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
989
|
+
if (!text) return '';
|
|
990
|
+
return path.resolve(text);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* read_chat node scope verdict. One physical daemon hosts a base node plus several
|
|
995
|
+
* worktree nodes; mesh_read_chat always dispatches read_chat with the requested
|
|
996
|
+
* node's workspace (`args.workspace`). When the resolved target session actually
|
|
997
|
+
* lives in a DIFFERENT worktree, returning its transcript — or worse, letting the
|
|
998
|
+
* native-history-by-workspace fallback splice sibling worktree turns into the
|
|
999
|
+
* reply — makes the coordinator believe one session received every worktree's
|
|
1000
|
+
* work. This guard refuses a CONFIRMED cross-workspace read instead of mixing.
|
|
1001
|
+
*
|
|
1002
|
+
* Conservative by design (mirrors the WTCLAIM fix-B "unknown → allow" rule): only
|
|
1003
|
+
* a session id that resolves to a known workspace which is unequal to a known
|
|
1004
|
+
* intended workspace blocks. When either side is unknown — no targetSessionId, no
|
|
1005
|
+
* args.workspace, an unregistered session, the coordinator self-session, or a
|
|
1006
|
+
* plain dashboard read that never passes a node workspace — the read proceeds
|
|
1007
|
+
* untouched, so base-node and same-daemon coordinator reads never regress.
|
|
1008
|
+
*/
|
|
1009
|
+
export function evaluateReadChatNodeWorkspaceScope(args: {
|
|
1010
|
+
targetSessionId?: string;
|
|
1011
|
+
intendedWorkspace?: string;
|
|
1012
|
+
sessionWorkspace?: string;
|
|
1013
|
+
}): { scoped: false } | { scoped: true; intended: string; actual: string } {
|
|
1014
|
+
const targetSessionId = typeof args.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
1015
|
+
if (!targetSessionId) return { scoped: false };
|
|
1016
|
+
const intended = normalizeMeshWorkspaceForCompare(args.intendedWorkspace);
|
|
1017
|
+
const actual = normalizeMeshWorkspaceForCompare(args.sessionWorkspace);
|
|
1018
|
+
if (!intended || !actual) return { scoped: false };
|
|
1019
|
+
if (intended === actual) return { scoped: false };
|
|
1020
|
+
return { scoped: true, intended, actual };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Resolve the target session's ACTUAL workspace from the most authoritative source
|
|
1025
|
+
* available on this daemon: the session registry record (stamped at register
|
|
1026
|
+
* time), then the live CLI adapter's working directory, then the bound instance
|
|
1027
|
+
* state. Returns '' when nothing knows the session's workspace — the caller treats
|
|
1028
|
+
* that as "unknown" and does not block.
|
|
1029
|
+
*/
|
|
1030
|
+
function resolveTargetSessionActualWorkspace(h: CommandHelpers, targetSessionId: string): string {
|
|
1031
|
+
const registryWorkspace = (h.ctx?.sessionRegistry?.get?.(targetSessionId) as any)?.workspace;
|
|
1032
|
+
if (typeof registryWorkspace === 'string' && registryWorkspace.trim()) return registryWorkspace;
|
|
1033
|
+
const adapter = h.getCliAdapter?.(targetSessionId);
|
|
1034
|
+
if (adapter && typeof adapter.workingDir === 'string' && adapter.workingDir.trim()) return adapter.workingDir;
|
|
1035
|
+
const instanceWorkspace = (getTargetInstance(h, { targetSessionId })?.getState?.() as any)?.workspace;
|
|
1036
|
+
if (typeof instanceWorkspace === 'string' && instanceWorkspace.trim()) return instanceWorkspace;
|
|
1037
|
+
return '';
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function isCurrentRuntimePtySafelyAttributed(args: {
|
|
1041
|
+
adapter: CliAdapter;
|
|
1042
|
+
helpers: CommandHelpers;
|
|
1043
|
+
readChatArgs: any;
|
|
1044
|
+
sessionWorkspace?: string;
|
|
1045
|
+
intendedWorkspace?: string;
|
|
1046
|
+
ptyMessages: ChatMessage[];
|
|
1047
|
+
}): boolean {
|
|
1048
|
+
if (args.adapter.cliType !== 'codex-cli') return false;
|
|
1049
|
+
if (!Array.isArray(args.ptyMessages) || args.ptyMessages.length === 0) return false;
|
|
1050
|
+
const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
|
|
1051
|
+
? args.readChatArgs.targetSessionId.trim()
|
|
1052
|
+
: '';
|
|
1053
|
+
const currentSession = args.helpers.currentSession as any;
|
|
1054
|
+
const currentSessionId = typeof currentSession?.sessionId === 'string'
|
|
1055
|
+
? currentSession.sessionId.trim()
|
|
1056
|
+
: '';
|
|
1057
|
+
if (!targetSessionId || !currentSessionId || targetSessionId !== currentSessionId) return false;
|
|
1058
|
+
|
|
1059
|
+
const runtimeMeta = typeof (args.adapter as any).getRuntimeMetadata === 'function'
|
|
1060
|
+
? (args.adapter as any).getRuntimeMetadata()
|
|
1061
|
+
: null;
|
|
1062
|
+
const runtimeId = typeof runtimeMeta?.runtimeId === 'string' ? runtimeMeta.runtimeId.trim() : '';
|
|
1063
|
+
if (!runtimeId || runtimeId !== targetSessionId) return false;
|
|
1064
|
+
const surfaceKind = typeof runtimeMeta?.surfaceKind === 'string' ? runtimeMeta.surfaceKind : '';
|
|
1065
|
+
if (surfaceKind === 'inactive_record' || surfaceKind === 'recovery_snapshot') return false;
|
|
1066
|
+
|
|
1067
|
+
const sessionWorkspace = normalizeComparableWorkspace(args.sessionWorkspace);
|
|
1068
|
+
const adapterWorkspace = normalizeComparableWorkspace(args.adapter.workingDir);
|
|
1069
|
+
if (!sessionWorkspace || !adapterWorkspace || sessionWorkspace !== adapterWorkspace) return false;
|
|
1070
|
+
const intendedWorkspace = normalizeComparableWorkspace(args.intendedWorkspace);
|
|
1071
|
+
if (intendedWorkspace && intendedWorkspace !== sessionWorkspace) return false;
|
|
1072
|
+
|
|
1073
|
+
const registryEntry = args.helpers.ctx?.sessionRegistry?.get?.(targetSessionId) as any;
|
|
1074
|
+
const registryInstanceKey = typeof registryEntry?.adapterKey === 'string' && registryEntry.adapterKey.trim()
|
|
1075
|
+
? registryEntry.adapterKey.trim()
|
|
1076
|
+
: typeof registryEntry?.instanceKey === 'string' && registryEntry.instanceKey.trim()
|
|
1077
|
+
? registryEntry.instanceKey.trim()
|
|
1078
|
+
: '';
|
|
1079
|
+
if (registryInstanceKey) {
|
|
1080
|
+
const targetInstance = args.helpers.ctx?.instanceManager?.getInstance?.(registryInstanceKey);
|
|
1081
|
+
if (targetInstance) {
|
|
1082
|
+
const instanceType = typeof (targetInstance as any).type === 'string' ? (targetInstance as any).type : '';
|
|
1083
|
+
if (instanceType && instanceType !== args.adapter.cliType) return false;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function supportsCliNativeTranscript(providerType: string, provider?: ProviderModule): boolean {
|
|
1091
|
+
// Preferred path: the provider module declares canonicalHistory in its
|
|
1092
|
+
// provider.json. We trust that declaration regardless of the legacy
|
|
1093
|
+
// allow-list. A2 will additionally require canonicalHistory.contractVersion
|
|
1094
|
+
// to be a supported value (transcript-v2.ts).
|
|
1095
|
+
if (provider?.category === 'cli' && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
// Last-resort fallback for early call sites where the provider module is
|
|
1099
|
+
// not yet loaded. Warn once per provider type so this dependency is visible
|
|
1100
|
+
// and can be removed in A2.
|
|
1101
|
+
if (CLI_NATIVE_TRANSCRIPT_PROVIDERS.has(providerType)) {
|
|
1102
|
+
warnLegacyNativeAllowlistHit(providerType);
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function getComparableVisibleText(message: ChatMessage | undefined): string {
|
|
1109
|
+
if (!message) return '';
|
|
1110
|
+
const role = String((message as any).role || '').trim().toLowerCase();
|
|
1111
|
+
if (role !== 'user' && role !== 'assistant') return '';
|
|
1112
|
+
const kind = String((message as any).kind || 'standard').trim().toLowerCase();
|
|
1113
|
+
if (kind && kind !== 'standard') return '';
|
|
1114
|
+
const content = flattenContent((message as any).content).replace(/\s+/g, ' ').trim();
|
|
1115
|
+
return content;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function hasOverlappingVisibleConversationText(nativeMessages: ChatMessage[], ptyMessages: ChatMessage[]): boolean {
|
|
1119
|
+
const nativeTexts = nativeMessages.map(getComparableVisibleText).filter(Boolean);
|
|
1120
|
+
const ptyTexts = ptyMessages.map(getComparableVisibleText).filter(Boolean);
|
|
1121
|
+
if (nativeTexts.length === 0 || ptyTexts.length === 0) return false;
|
|
1122
|
+
for (const nativeText of nativeTexts) {
|
|
1123
|
+
for (const ptyText of ptyTexts) {
|
|
1124
|
+
if (nativeText === ptyText) return true;
|
|
1125
|
+
const shorter = nativeText.length <= ptyText.length ? nativeText : ptyText;
|
|
1126
|
+
const longer = nativeText.length <= ptyText.length ? ptyText : nativeText;
|
|
1127
|
+
if (shorter.length >= 32 && longer.includes(shorter)) return true;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function hasSafeNativeHistoryMapping(args: {
|
|
1134
|
+
historySessionId?: string;
|
|
1135
|
+
providerSessionId?: string;
|
|
1136
|
+
workspace?: string;
|
|
1137
|
+
nativeMessages: ChatMessage[];
|
|
1138
|
+
ptyMessages?: ChatMessage[];
|
|
1139
|
+
requireWorkspaceContentOverlap?: boolean;
|
|
1140
|
+
}): boolean {
|
|
1141
|
+
const isCoordinatorTranscript = args.nativeMessages.some((m: any) => {
|
|
1142
|
+
const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
|
|
1143
|
+
return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat') || text.includes('mesh_launch_session');
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
|
|
1147
|
+
if (explicitSessionId) {
|
|
1148
|
+
const expectedWorkspace = normalizeComparableWorkspace(args.workspace);
|
|
1149
|
+
const declaredWorkspaces = args.nativeMessages
|
|
1150
|
+
.map((message: any) => normalizeComparableWorkspace(message?.workspace))
|
|
1151
|
+
.filter(Boolean);
|
|
1152
|
+
if (
|
|
1153
|
+
expectedWorkspace
|
|
1154
|
+
&& declaredWorkspaces.length > 0
|
|
1155
|
+
&& !declaredWorkspaces.some((workspace) => workspace === expectedWorkspace)
|
|
1156
|
+
) {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
const messageSessionIds = args.nativeMessages
|
|
1160
|
+
.map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
|
|
1161
|
+
.filter(Boolean);
|
|
1162
|
+
if (messageSessionIds.length > 0) {
|
|
1163
|
+
return messageSessionIds.some((id) => id === explicitSessionId);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Messages carry no historySessionId — cannot confirm they belong to the requested session.
|
|
1167
|
+
// Only allow a coordinator transcript that is also confirmed by the PTY side; otherwise
|
|
1168
|
+
// fail closed so a same-workspace session's history is never silently accepted.
|
|
1169
|
+
if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
|
|
1170
|
+
const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
|
|
1171
|
+
const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
|
|
1172
|
+
return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
|
|
1173
|
+
});
|
|
1174
|
+
return ptyHasCoordinator;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// No historySessionId in messages and no coordinator cross-check: fail closed.
|
|
1178
|
+
// Workspace-only matching must not override an explicit session identity.
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
const workspace = String(args.workspace || '').trim();
|
|
1182
|
+
if (!workspace) return false;
|
|
1183
|
+
const workspaceMatches = args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
|
|
1184
|
+
if (!workspaceMatches) return false;
|
|
1185
|
+
|
|
1186
|
+
if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
|
|
1187
|
+
const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
|
|
1188
|
+
const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
|
|
1189
|
+
return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
|
|
1190
|
+
});
|
|
1191
|
+
if (!ptyHasCoordinator) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (!args.requireWorkspaceContentOverlap) return true;
|
|
1197
|
+
return hasOverlappingVisibleConversationText(args.nativeMessages, args.ptyMessages || []);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Provenance boundary: workspace-only native history lookup is never safe
|
|
1201
|
+
// because multiple concurrent sessions sharing the same cwd would alias each
|
|
1202
|
+
// other. historySessionId (the provider-native session key) is required to
|
|
1203
|
+
// establish ownership. hasSafeNativeHistoryMapping() enforces the same
|
|
1204
|
+
// invariant after the read; both guards must hold for native history to be used.
|
|
1205
|
+
/**
|
|
1206
|
+
* Pull the session's spawnedAtMs out of the registry. Native-history
|
|
1207
|
+
* file pickers use it as a "files older than this can't be from this
|
|
1208
|
+
* session" floor; without it a fresh dashboard view would inherit the
|
|
1209
|
+
* previous session's transcript whenever its file happened to be the
|
|
1210
|
+
* newest match. Returns undefined when the session isn't registered
|
|
1211
|
+
* (e.g. read_chat before the live session was wired up) — the executor
|
|
1212
|
+
* treats undefined as "no floor".
|
|
1213
|
+
*/
|
|
1214
|
+
function sessionStartedAtMsFromRegistry(h: CommandHelpers, targetSessionId: string | undefined): number | undefined {
|
|
1215
|
+
const sid = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
|
|
1216
|
+
if (!sid) return undefined;
|
|
1217
|
+
const target = h.ctx?.sessionRegistry?.get?.(sid);
|
|
1218
|
+
return typeof target?.spawnedAtMs === 'number' ? target.spawnedAtMs : undefined;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Pull the env vars the daemon set when it spawned this session's CLI.
|
|
1223
|
+
* Mesh coordinator points hermes at a per-coordinator HERMES_HOME so
|
|
1224
|
+
* the native-history reader needs that override to find the right
|
|
1225
|
+
* state.db; without it the reader sees ~/.hermes/state.db and misses
|
|
1226
|
+
* every coordinator-session transcript.
|
|
1227
|
+
*
|
|
1228
|
+
* Returns undefined when no SpecCliAdapter is in play (legacy
|
|
1229
|
+
* providers / CDP) or when the adapter exposes no spawn env.
|
|
1230
|
+
*/
|
|
1231
|
+
function sessionSpawnEnvFromAdapter(h: CommandHelpers, targetSessionId: string | undefined): Record<string, string> | undefined {
|
|
1232
|
+
const adapter = getTargetedCliAdapter(h, { targetSessionId }, undefined);
|
|
1233
|
+
if (!adapter || typeof adapter.getRuntimeMetadata !== 'function') return undefined;
|
|
1234
|
+
const meta = adapter.getRuntimeMetadata() as Record<string, unknown> | undefined;
|
|
1235
|
+
const env = meta && typeof meta === 'object' ? (meta as Record<string, unknown>).spawnedEnv : undefined;
|
|
1236
|
+
return env && typeof env === 'object' ? env as Record<string, string> : undefined;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
function readCliProviderNativeHistory(agentStr: string, args: {
|
|
1241
|
+
canonicalHistory?: ProviderModule['canonicalHistory'];
|
|
1242
|
+
historySessionId?: string;
|
|
1243
|
+
workspace?: string;
|
|
1244
|
+
offset: number;
|
|
1245
|
+
limit: number;
|
|
1246
|
+
excludeRecentCount: number;
|
|
1247
|
+
historyBehavior?: ProviderModule['historyBehavior'];
|
|
1248
|
+
scripts?: ProviderScripts;
|
|
1249
|
+
excludeInProgressTurn?: boolean;
|
|
1250
|
+
sessionStartedAtMs?: number;
|
|
1251
|
+
envOverrides?: Record<string, string>;
|
|
1252
|
+
}): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
|
|
1253
|
+
const canBindFromLiveSession = !args.historySessionId
|
|
1254
|
+
&& typeof args.sessionStartedAtMs === 'number'
|
|
1255
|
+
&& args.sessionStartedAtMs > 0
|
|
1256
|
+
&& typeof args.workspace === 'string'
|
|
1257
|
+
&& args.workspace.trim().length > 0;
|
|
1258
|
+
if (!args.historySessionId && !canBindFromLiveSession) {
|
|
1259
|
+
return {
|
|
1260
|
+
messages: [],
|
|
1261
|
+
hasMore: false,
|
|
1262
|
+
source: 'native-unavailable',
|
|
1263
|
+
unavailableReason: 'native_history_workspace_only_lookup_unsafe',
|
|
1264
|
+
lookup: 'session',
|
|
1265
|
+
} as ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' };
|
|
1266
|
+
}
|
|
1267
|
+
const sessionHistory = readProviderChatHistory(agentStr, {
|
|
1268
|
+
canonicalHistory: args.canonicalHistory,
|
|
1269
|
+
historySessionId: args.historySessionId,
|
|
1270
|
+
workspace: args.workspace,
|
|
1271
|
+
offset: args.offset,
|
|
1272
|
+
limit: args.limit,
|
|
1273
|
+
excludeRecentCount: args.excludeRecentCount,
|
|
1274
|
+
historyBehavior: args.historyBehavior,
|
|
1275
|
+
scripts: args.scripts as any,
|
|
1276
|
+
excludeInProgressTurn: args.excludeInProgressTurn,
|
|
1277
|
+
sessionStartedAtMs: args.sessionStartedAtMs,
|
|
1278
|
+
envOverrides: args.envOverrides,
|
|
1279
|
+
});
|
|
1280
|
+
const boundProviderSessionId = typeof (sessionHistory as any)?.providerSessionId === 'string'
|
|
1281
|
+
? (sessionHistory as any).providerSessionId.trim()
|
|
1282
|
+
: '';
|
|
1283
|
+
// A fresh live session can be bound without a provider id when the native
|
|
1284
|
+
// reader matched both cwd and session_meta.timestamp to spawnedAtMs.
|
|
1285
|
+
return {
|
|
1286
|
+
...(sessionHistory as any),
|
|
1287
|
+
lookup: args.historySessionId || (canBindFromLiveSession && boundProviderSessionId)
|
|
1288
|
+
? 'session'
|
|
1289
|
+
: 'workspace',
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function readLiveCodexWorkspaceNativeHistory(agentStr: string, args: {
|
|
1294
|
+
canonicalHistory?: ProviderModule['canonicalHistory'];
|
|
1295
|
+
workspace?: string;
|
|
1296
|
+
offset: number;
|
|
1297
|
+
limit: number;
|
|
1298
|
+
excludeRecentCount: number;
|
|
1299
|
+
historyBehavior?: ProviderModule['historyBehavior'];
|
|
1300
|
+
scripts?: ProviderScripts;
|
|
1301
|
+
}): (ReturnType<typeof readProviderChatHistory> & { lookup: 'workspace' }) | null {
|
|
1302
|
+
if (agentStr !== 'codex-cli') return null;
|
|
1303
|
+
const workspace = typeof args.workspace === 'string' ? args.workspace.trim() : '';
|
|
1304
|
+
if (!workspace) return null;
|
|
1305
|
+
const history = readProviderChatHistory(agentStr, {
|
|
1306
|
+
canonicalHistory: args.canonicalHistory,
|
|
1307
|
+
workspace,
|
|
1308
|
+
offset: args.offset,
|
|
1309
|
+
limit: args.limit,
|
|
1310
|
+
excludeRecentCount: args.excludeRecentCount,
|
|
1311
|
+
historyBehavior: args.historyBehavior,
|
|
1312
|
+
scripts: args.scripts as any,
|
|
1313
|
+
});
|
|
1314
|
+
return { ...(history as any), lookup: 'workspace' };
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// (A2.2) isNativeHistoryFreshEnough removed. The v1 freshness comparison
|
|
1318
|
+
// (native_newest vs pty_newest with a 5-minute mtime grace window) was the
|
|
1319
|
+
// direct cause of the plipping behaviour: PTY arrived every turn so native
|
|
1320
|
+
// looked stale by default. ChatSourceMachine never compares native vs PTY
|
|
1321
|
+
// freshness — the lock holds across arbitrary PTY arrival. See
|
|
1322
|
+
// chat/source-machine.ts for the new semantics.
|
|
1323
|
+
|
|
1324
|
+
function shouldPreserveReadChatPayloadField(key: string): boolean {
|
|
1325
|
+
return key === 'messageSource' || key === 'transcriptProvenance';
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function updateMessageSourceReturnedCount(value: unknown, returnedMessageCount: number): unknown {
|
|
1329
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
|
|
1330
|
+
const record = value as Record<string, unknown>;
|
|
1331
|
+
const coverage = record.coverage && typeof record.coverage === 'object' && !Array.isArray(record.coverage)
|
|
1332
|
+
? record.coverage as Record<string, unknown>
|
|
1333
|
+
: undefined;
|
|
1334
|
+
if (!coverage) return value;
|
|
1335
|
+
return {
|
|
1336
|
+
...record,
|
|
1337
|
+
coverage: {
|
|
1338
|
+
...coverage,
|
|
1339
|
+
returnedMessageCount,
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
224
1343
|
|
|
225
1344
|
function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
|
|
226
1345
|
const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
|
|
@@ -281,18 +1400,46 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
|
|
|
281
1400
|
}
|
|
282
1401
|
switch (raw) {
|
|
283
1402
|
case 'starting':
|
|
284
|
-
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : '
|
|
1403
|
+
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'starting';
|
|
285
1404
|
case 'stopped':
|
|
286
1405
|
case 'disconnected':
|
|
287
1406
|
case 'not_monitored':
|
|
288
1407
|
return 'error';
|
|
1408
|
+
case 'waiting_approval':
|
|
1409
|
+
// The contract validator requires activeModal+buttons whenever
|
|
1410
|
+
// status is waiting_approval. If a producer/coercer set this
|
|
1411
|
+
// status without staging the modal yet (a race we hit with
|
|
1412
|
+
// codex-cli during tool approval setup), downgrade to a
|
|
1413
|
+
// generating-like status so readChat still returns successfully.
|
|
1414
|
+
// The next poll will pick up the modal once the provider has
|
|
1415
|
+
// emitted it.
|
|
1416
|
+
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
|
|
289
1417
|
default:
|
|
290
1418
|
return raw;
|
|
291
1419
|
}
|
|
292
1420
|
}
|
|
293
1421
|
|
|
294
1422
|
function isGeneratingLikeStatus(status: unknown): boolean {
|
|
295
|
-
return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
|
|
1423
|
+
return status === 'generating' || status === 'streaming' || status === 'no_progress' || status === 'long_generating' || status === 'starting';
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function hasVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
|
|
1427
|
+
if (!Array.isArray(messages)) return false;
|
|
1428
|
+
return messages.some((message: any) => {
|
|
1429
|
+
if (!message || message.role !== 'assistant') return false;
|
|
1430
|
+
const kind = typeof message.kind === 'string' ? message.kind : 'standard';
|
|
1431
|
+
if (kind !== 'standard') return false;
|
|
1432
|
+
return String(message.content || '').trim().length > 0;
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function hasFinalVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
|
|
1437
|
+
if (!Array.isArray(messages)) return false;
|
|
1438
|
+
const visible = filterUserFacingChatMessages(messages as ChatMessage[]);
|
|
1439
|
+
const last = visible[visible.length - 1] as ChatMessage | undefined;
|
|
1440
|
+
const role = typeof last?.role === 'string' ? last.role.trim().toLowerCase() : '';
|
|
1441
|
+
const content = last ? flattenContent(last.content).trim() : '';
|
|
1442
|
+
return (role === 'assistant' || role === 'model') && content.length > 0;
|
|
296
1443
|
}
|
|
297
1444
|
|
|
298
1445
|
function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
|
|
@@ -304,7 +1451,26 @@ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal:
|
|
|
304
1451
|
return true;
|
|
305
1452
|
}
|
|
306
1453
|
|
|
307
|
-
function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
|
|
1454
|
+
function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any, parsedMessages?: unknown[]): string {
|
|
1455
|
+
const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
1456
|
+
if (adapterRawStatus === 'starting'
|
|
1457
|
+
&& isGeneratingLikeStatus(parsedStatus)
|
|
1458
|
+
&& !hasNonEmptyModalButtons(activeModal)
|
|
1459
|
+
&& Array.isArray(parsedMessages)
|
|
1460
|
+
&& parsedMessages.length === 0
|
|
1461
|
+
&& Array.isArray(adapterStatus?.messages)
|
|
1462
|
+
&& adapterStatus.messages.length === 0
|
|
1463
|
+
&& !(typeof adapter.isProcessing === 'function' && adapter.isProcessing())) {
|
|
1464
|
+
return 'starting';
|
|
1465
|
+
}
|
|
1466
|
+
if (
|
|
1467
|
+
isGeneratingLikeStatus(adapterRawStatus)
|
|
1468
|
+
&& parsedStatus === 'idle'
|
|
1469
|
+
&& !hasNonEmptyModalButtons(activeModal)
|
|
1470
|
+
&& !hasVisibleAssistantMessage(parsedMessages)
|
|
1471
|
+
) {
|
|
1472
|
+
return adapterRawStatus;
|
|
1473
|
+
}
|
|
308
1474
|
if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
|
|
309
1475
|
return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
|
|
310
1476
|
}
|
|
@@ -325,7 +1491,65 @@ function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: stri
|
|
|
325
1491
|
});
|
|
326
1492
|
}
|
|
327
1493
|
|
|
328
|
-
|
|
1494
|
+
/**
|
|
1495
|
+
* Collapse adjacent PTY messages whose canonical (whitespace-stripped)
|
|
1496
|
+
* content is identical, OR whose turn key + role/kind match.
|
|
1497
|
+
*
|
|
1498
|
+
* The PTY parser of some providers (hermes-cli observed in the wild)
|
|
1499
|
+
* emits the same logical assistant turn twice when the terminal re-wraps
|
|
1500
|
+
* the text at a different column. The two emissions differ in newline
|
|
1501
|
+
* position — and sometimes in a single inserted space next to punctuation
|
|
1502
|
+
* (e.g. `(수정 2개), upstream` vs `(수정 2개 ), upstream`), so a simple
|
|
1503
|
+
* `\s+ -> ' '` normalize cannot collapse them.
|
|
1504
|
+
*
|
|
1505
|
+
* Strategy:
|
|
1506
|
+
* 1. If both messages carry the same _turnKey + role + kind, they are
|
|
1507
|
+
* the same logical turn by construction. Collapse.
|
|
1508
|
+
* 2. Otherwise compare with all whitespace stripped — wrap variants
|
|
1509
|
+
* collapse to identical strings.
|
|
1510
|
+
*
|
|
1511
|
+
* Native-history paths run through pageHistoryRecords and already
|
|
1512
|
+
* collapse on a normalized signature; this helper is the PTY equivalent
|
|
1513
|
+
* the readChat sync path was missing.
|
|
1514
|
+
*/
|
|
1515
|
+
function collapseAdjacentDuplicateChatMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
1516
|
+
if (!Array.isArray(messages) || messages.length <= 1) return messages;
|
|
1517
|
+
const result: ChatMessage[] = [];
|
|
1518
|
+
let prevRoleKind = '';
|
|
1519
|
+
let prevStripped = '';
|
|
1520
|
+
for (const message of messages) {
|
|
1521
|
+
const role = typeof message.role === 'string' ? message.role : '';
|
|
1522
|
+
const kind = typeof message.kind === 'string' ? message.kind : 'standard';
|
|
1523
|
+
const content = typeof message.content === 'string'
|
|
1524
|
+
? message.content
|
|
1525
|
+
: (Array.isArray(message.content) ? message.content.map((p: any) => typeof p?.text === 'string' ? p.text : '').join('') : '');
|
|
1526
|
+
const strippedContent = content.replace(/\s+/g, '');
|
|
1527
|
+
// Empty content or system messages are passed through untouched.
|
|
1528
|
+
if (!strippedContent || role === 'system') {
|
|
1529
|
+
result.push(message);
|
|
1530
|
+
prevRoleKind = '';
|
|
1531
|
+
prevStripped = '';
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const roleKind = `${role}:${kind}`;
|
|
1535
|
+
const sameStripped = strippedContent === prevStripped && roleKind === prevRoleKind;
|
|
1536
|
+
if (result.length > 0 && sameStripped) {
|
|
1537
|
+
// Adjacent duplicate after stripping all whitespace. Keep the
|
|
1538
|
+
// *later* copy because PTY's last emission usually has the most
|
|
1539
|
+
// complete formatting.
|
|
1540
|
+
result[result.length - 1] = message;
|
|
1541
|
+
prevRoleKind = roleKind;
|
|
1542
|
+
prevStripped = strippedContent;
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
result.push(message);
|
|
1546
|
+
prevRoleKind = roleKind;
|
|
1547
|
+
prevStripped = strippedContent;
|
|
1548
|
+
}
|
|
1549
|
+
return result;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function buildReadChatCommandResult(payload: Record<string, any>, args: any, h?: CommandHelpers): CommandResult {
|
|
329
1553
|
let validatedPayload: Record<string, any>;
|
|
330
1554
|
const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
|
|
331
1555
|
? payload.debugReadChat
|
|
@@ -339,9 +1563,40 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
|
|
|
339
1563
|
return { success: false, error: error?.message || String(error) };
|
|
340
1564
|
}
|
|
341
1565
|
const messages = normalizeReadChatMessages(validatedPayload);
|
|
342
|
-
|
|
1566
|
+
// Last-mile coordinator-prompt filter. Different read_chat code paths
|
|
1567
|
+
// produce the final messages array (native-history main path, codex
|
|
1568
|
+
// exact-runtime-mirror fallback, daemon-side pty-parser, etc), so
|
|
1569
|
+
// applying it here means we don't have to thread the filter through
|
|
1570
|
+
// every one. Driven by the provider setting `showCoordinatorSystemPrompt`
|
|
1571
|
+
// + the coordinator-registry entry for the target session.
|
|
1572
|
+
const sessionIdHint = typeof args?.targetSessionId === 'string' ? args.targetSessionId
|
|
1573
|
+
: typeof args?.sessionId === 'string' ? args.sessionId
|
|
1574
|
+
: '';
|
|
1575
|
+
const providerHint = typeof args?.cliType === 'string' ? args.cliType
|
|
1576
|
+
: typeof args?.providerType === 'string' ? args.providerType
|
|
1577
|
+
: typeof args?.agentType === 'string' ? args.agentType
|
|
1578
|
+
: '';
|
|
1579
|
+
const filteredMessages = h
|
|
1580
|
+
? maybeHideCoordinatorPromptMessage(h, providerHint, sessionIdHint, messages)
|
|
1581
|
+
: messages;
|
|
1582
|
+
// By default read_chat returns only user-facing prose turns. When the
|
|
1583
|
+
// caller opts in with `includeActivity`, tool/terminal/thought activity
|
|
1584
|
+
// bubbles (e.g. the native transcript's tool calls and results) are kept
|
|
1585
|
+
// inline too, in chronological order, so a restored conversation can show
|
|
1586
|
+
// what the agent actually did — not just the prose around it.
|
|
1587
|
+
const includeActivity = args?.includeActivity === true || args?.includeActivity === 'true';
|
|
1588
|
+
const visibleMessages = includeActivity
|
|
1589
|
+
? filteredMessages.filter((m) => isUserFacingChatMessage(m) || isActivityChatMessage(m))
|
|
1590
|
+
: filterUserFacingChatMessages(filteredMessages);
|
|
343
1591
|
const sync = buildFullTail(visibleMessages, normalizeReadChatTailLimit(args));
|
|
344
1592
|
const hiddenMsgCount = Math.max(0, messages.length - visibleMessages.length);
|
|
1593
|
+
const preservedPayloadFields = Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key)));
|
|
1594
|
+
if (preservedPayloadFields.messageSource) {
|
|
1595
|
+
preservedPayloadFields.messageSource = updateMessageSourceReturnedCount(preservedPayloadFields.messageSource, sync.messages.length);
|
|
1596
|
+
}
|
|
1597
|
+
if (preservedPayloadFields.transcriptProvenance) {
|
|
1598
|
+
preservedPayloadFields.transcriptProvenance = updateMessageSourceReturnedCount(preservedPayloadFields.transcriptProvenance, sync.messages.length);
|
|
1599
|
+
}
|
|
345
1600
|
const returnedDebugReadChat = debugReadChat
|
|
346
1601
|
? {
|
|
347
1602
|
...debugReadChat,
|
|
@@ -356,6 +1611,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
|
|
|
356
1611
|
return {
|
|
357
1612
|
success: true,
|
|
358
1613
|
...validatedPayload,
|
|
1614
|
+
...preservedPayloadFields,
|
|
359
1615
|
messages: sync.messages,
|
|
360
1616
|
totalMessages: sync.totalMessages,
|
|
361
1617
|
...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
|
|
@@ -456,7 +1712,7 @@ function summarizeProviderForDebug(provider: ProviderModule | undefined): Record
|
|
|
456
1712
|
name: provider.name,
|
|
457
1713
|
category: provider.category,
|
|
458
1714
|
version: (provider as any).version,
|
|
459
|
-
canonicalHistory: provider.
|
|
1715
|
+
canonicalHistory: provider.nativeHistory,
|
|
460
1716
|
historyBehavior: provider.historyBehavior,
|
|
461
1717
|
webviewMatchText: provider.webviewMatchText,
|
|
462
1718
|
scriptNames: scripts,
|
|
@@ -508,8 +1764,10 @@ function summarizeStateForDebug(state: any): Record<string, unknown> | null {
|
|
|
508
1764
|
title: activeChat.title,
|
|
509
1765
|
messageCount: Array.isArray(activeChat.messages) ? activeChat.messages.length : undefined,
|
|
510
1766
|
activeModal: activeChat.activeModal,
|
|
1767
|
+
activeInteractivePrompt: activeChat.activeInteractivePrompt ?? null,
|
|
511
1768
|
messagesTail: Array.isArray(activeChat.messages) ? activeChat.messages.slice(-10) : undefined,
|
|
512
1769
|
} : null,
|
|
1770
|
+
activeInteractivePrompt: (state as { activeInteractivePrompt?: unknown }).activeInteractivePrompt ?? null,
|
|
513
1771
|
controlValues: state.controlValues,
|
|
514
1772
|
summaryMetadata: state.summaryMetadata,
|
|
515
1773
|
};
|
|
@@ -584,6 +1842,8 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
|
|
|
584
1842
|
adapterStatus: debugReadChat.adapterStatus,
|
|
585
1843
|
parsedStatus: debugReadChat.parsedStatus,
|
|
586
1844
|
returnedStatus: debugReadChat.returnedStatus,
|
|
1845
|
+
selectedMessageSource: debugReadChat.selectedMessageSource,
|
|
1846
|
+
messageSource: debugReadChat.messageSource,
|
|
587
1847
|
parsedMsgCount: debugReadChat.parsedMsgCount,
|
|
588
1848
|
returnedMsgCount: debugReadChat.returnedMsgCount,
|
|
589
1849
|
shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
|
|
@@ -646,11 +1906,20 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
|
|
|
646
1906
|
providerSessionId: readResult.providerSessionId,
|
|
647
1907
|
transcriptAuthority: readResult.transcriptAuthority,
|
|
648
1908
|
coverage: readResult.coverage,
|
|
1909
|
+
messageSource: readResult.messageSource,
|
|
1910
|
+
transcriptProvenance: readResult.transcriptProvenance,
|
|
649
1911
|
activeModal: readResult.activeModal,
|
|
650
1912
|
messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
|
|
651
1913
|
debugReadChat: readResult.debugReadChat,
|
|
652
1914
|
}
|
|
653
|
-
: {
|
|
1915
|
+
: {
|
|
1916
|
+
success: false,
|
|
1917
|
+
error: readResult.error,
|
|
1918
|
+
code: readResult.code,
|
|
1919
|
+
messageSource: readResult.messageSource,
|
|
1920
|
+
transcriptProvenance: readResult.transcriptProvenance,
|
|
1921
|
+
debugReadChat: readResult.debugReadChat,
|
|
1922
|
+
};
|
|
654
1923
|
} catch (error: any) {
|
|
655
1924
|
readChat = { success: false, error: error?.message || String(error) };
|
|
656
1925
|
}
|
|
@@ -693,6 +1962,9 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
|
|
|
693
1962
|
ready: typeof adapter.isReady === 'function' ? adapter.isReady() : undefined,
|
|
694
1963
|
processing: typeof adapter.isProcessing === 'function' ? adapter.isProcessing() : undefined,
|
|
695
1964
|
debugSnapshot: adapterDebugSnapshot,
|
|
1965
|
+
scriptInvocationTrace: typeof (adapter as any).getScriptInvocationTrace === 'function'
|
|
1966
|
+
? (adapter as any).getScriptInvocationTrace()
|
|
1967
|
+
: undefined,
|
|
696
1968
|
} : null,
|
|
697
1969
|
readChat,
|
|
698
1970
|
frontend: args?.frontendSnapshot && typeof args.frontendSnapshot === 'object' ? args.frontendSnapshot : null,
|
|
@@ -817,16 +2089,58 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
817
2089
|
: typeof (h.currentSession as any)?.workspace === 'string'
|
|
818
2090
|
? (h.currentSession as any).workspace
|
|
819
2091
|
: undefined;
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
historySessionId
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
2092
|
+
const exactNativeHistoryScope = Boolean(
|
|
2093
|
+
(typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
|
|
2094
|
+
|| (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
|
|
2095
|
+
|| (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
|
|
2096
|
+
);
|
|
2097
|
+
const result = supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)
|
|
2098
|
+
? readCliProviderNativeHistory(agentStr, {
|
|
2099
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2100
|
+
historySessionId,
|
|
2101
|
+
workspace,
|
|
2102
|
+
offset: offset || 0,
|
|
2103
|
+
limit: limit || 30,
|
|
2104
|
+
excludeRecentCount,
|
|
2105
|
+
historyBehavior: provider?.historyBehavior,
|
|
2106
|
+
scripts: provider?.scripts as any,
|
|
2107
|
+
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
2108
|
+
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
2109
|
+
})
|
|
2110
|
+
: readProviderChatHistory(agentStr, {
|
|
2111
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2112
|
+
historySessionId,
|
|
2113
|
+
workspace,
|
|
2114
|
+
offset: offset || 0,
|
|
2115
|
+
limit: limit || 30,
|
|
2116
|
+
excludeRecentCount,
|
|
2117
|
+
historyBehavior: provider?.historyBehavior,
|
|
2118
|
+
scripts: provider?.scripts as any,
|
|
2119
|
+
});
|
|
2120
|
+
if (supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
|
|
2121
|
+
const lookup = (result as any).lookup === 'workspace' ? 'workspace' : 'session';
|
|
2122
|
+
const messages = Array.isArray((result as any).messages)
|
|
2123
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, (result as any).messages as ChatMessage[], (result as any)?.providerSessionId)
|
|
2124
|
+
: [];
|
|
2125
|
+
const historyProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
|
|
2126
|
+
? (result as any).providerSessionId
|
|
2127
|
+
: readHistorySessionIdFromMessages(messages) || historySessionId;
|
|
2128
|
+
const safeMapping = hasSafeNativeHistoryMapping({
|
|
2129
|
+
historySessionId: lookup === 'workspace' ? undefined : historySessionId,
|
|
2130
|
+
providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
|
|
2131
|
+
workspace,
|
|
2132
|
+
nativeMessages: messages,
|
|
2133
|
+
});
|
|
2134
|
+
if ((result as any).source === 'provider-native' && messages.length > 0 && !safeMapping) {
|
|
2135
|
+
return {
|
|
2136
|
+
success: true,
|
|
2137
|
+
messages: [],
|
|
2138
|
+
hasMore: false,
|
|
2139
|
+
source: 'native-unavailable',
|
|
2140
|
+
agent: agentStr,
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
830
2144
|
return { success: true, ...result, agent: agentStr };
|
|
831
2145
|
} catch (e: any) {
|
|
832
2146
|
return { success: false, error: e.message };
|
|
@@ -834,7 +2148,48 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
834
2148
|
}
|
|
835
2149
|
|
|
836
2150
|
export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
|
|
837
|
-
|
|
2151
|
+
// Node scope guard: a daemon hosting a base node + several worktree nodes must
|
|
2152
|
+
// not serve worktree A's transcript (or splice sibling worktree turns via the
|
|
2153
|
+
// native-history-by-workspace fallback) when a coordinator scoped the read to
|
|
2154
|
+
// worktree B. mesh_read_chat always passes the requested node's workspace as
|
|
2155
|
+
// args.workspace; refuse a CONFIRMED cross-workspace read rather than mix.
|
|
2156
|
+
{
|
|
2157
|
+
const guardSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
2158
|
+
if (guardSessionId && typeof args?.workspace === 'string' && args.workspace.trim()) {
|
|
2159
|
+
const verdict = evaluateReadChatNodeWorkspaceScope({
|
|
2160
|
+
targetSessionId: guardSessionId,
|
|
2161
|
+
intendedWorkspace: args.workspace,
|
|
2162
|
+
sessionWorkspace: resolveTargetSessionActualWorkspace(h, guardSessionId),
|
|
2163
|
+
});
|
|
2164
|
+
if (verdict.scoped) {
|
|
2165
|
+
LOG.info('Command', `[read_chat] node scope mismatch: session ${guardSessionId} workspace "${verdict.actual}" ≠ requested node workspace "${verdict.intended}" — refusing cross-worktree transcript`);
|
|
2166
|
+
return {
|
|
2167
|
+
success: false,
|
|
2168
|
+
code: 'read_chat_session_node_scope_mismatch',
|
|
2169
|
+
error: `Session ${guardSessionId} belongs to a different worktree (workspace "${verdict.actual}") than the requested node (workspace "${verdict.intended}"). Refusing to return a cross-worktree transcript — target the node that owns this session.`,
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
// Resolve provider in order: explicit agentType/providerType > registered session.
|
|
2175
|
+
// Without this fallback, callers that only have a sessionId (e.g. a chat tail
|
|
2176
|
+
// controller that just got handed a session ID over WS) get an empty result
|
|
2177
|
+
// because getProvider(undefined) returns undefined and the rest of the pipeline
|
|
2178
|
+
// bails. This makes the UI look like the session "disappeared".
|
|
2179
|
+
let providerHint: string | undefined = args?.agentType || args?.providerType;
|
|
2180
|
+
if (!providerHint) {
|
|
2181
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
2182
|
+
if (targetSessionId) {
|
|
2183
|
+
const session = (h.ctx as any)?.sessionRegistry?.get?.(targetSessionId);
|
|
2184
|
+
if (session && typeof session.providerType === 'string') {
|
|
2185
|
+
providerHint = session.providerType;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (!providerHint && h.currentSession?.providerType) {
|
|
2189
|
+
providerHint = h.currentSession.providerType;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
const provider = h.getProvider(providerHint);
|
|
838
2193
|
const transport = getTargetTransport(h, provider);
|
|
839
2194
|
const historySessionId = getHistorySessionId(h, args);
|
|
840
2195
|
|
|
@@ -874,66 +2229,541 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
874
2229
|
? parsedRecord.coverage
|
|
875
2230
|
: undefined;
|
|
876
2231
|
const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
|
|
877
|
-
const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
|
|
2232
|
+
const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus, parsedRecord.messages);
|
|
878
2233
|
const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
|
|
879
|
-
const parsedMessages =
|
|
2234
|
+
const parsedMessages = collapseAdjacentDuplicateChatMessages(
|
|
2235
|
+
finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus),
|
|
2236
|
+
);
|
|
880
2237
|
const returnedMessages = runtimeMessageMerger?.category === 'cli'
|
|
881
2238
|
&& runtimeMessageMerger.type === adapter.cliType
|
|
882
2239
|
&& typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
|
|
883
2240
|
? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
|
|
884
2241
|
: parsedMessages;
|
|
2242
|
+
const providerType = provider?.type || adapter.cliType;
|
|
2243
|
+
let selectedMessages = returnedMessages;
|
|
2244
|
+
let selectedTitle = title;
|
|
2245
|
+
let selectedProviderSessionId = providerSessionId;
|
|
2246
|
+
let selectedTranscriptAuthority = transcriptAuthority;
|
|
2247
|
+
let selectedCoverage = coverage;
|
|
2248
|
+
let selectedStatus = returnedStatus;
|
|
2249
|
+
const _targetSidForWs = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
2250
|
+
const _registryWs = _targetSidForWs
|
|
2251
|
+
? (h.ctx?.sessionRegistry?.get?.(_targetSidForWs) as any)?.workspace
|
|
2252
|
+
: undefined;
|
|
2253
|
+
const _currentSessionWs = typeof (h.currentSession as any)?.workspace === 'string'
|
|
2254
|
+
? (h.currentSession as any).workspace
|
|
2255
|
+
: typeof adapter.workingDir === 'string'
|
|
2256
|
+
? adapter.workingDir
|
|
2257
|
+
: undefined;
|
|
2258
|
+
const sessionWorkspace = _targetSidForWs
|
|
2259
|
+
? (typeof _registryWs === 'string' ? _registryWs : (typeof args?.workspace === 'string' ? args.workspace : undefined) ?? _currentSessionWs)
|
|
2260
|
+
: _currentSessionWs;
|
|
2261
|
+
const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
|
|
2262
|
+
// ───────────────────────────────────────────────────────────
|
|
2263
|
+
// Chat source decision via ChatSourceMachine (A2 big-bang).
|
|
2264
|
+
// Replaces the ~300-line if-ladder that mixed source decision
|
|
2265
|
+
// with native fetch, anchor mutation, and runtime mirror
|
|
2266
|
+
// selection. The machine decides only between native-history
|
|
2267
|
+
// and pty-parser; downstream selection of which message array
|
|
2268
|
+
// to surface stays here.
|
|
2269
|
+
//
|
|
2270
|
+
// Behavioural changes vs v1:
|
|
2271
|
+
// - No more nativeHistoryAnchoredAt mutation on the adapter.
|
|
2272
|
+
// Lock state lives in CHAT_SOURCE_REGISTRY keyed by
|
|
2273
|
+
// (providerType, sessionId).
|
|
2274
|
+
// - No PTY-vs-native freshness comparison. The lock holds
|
|
2275
|
+
// across arbitrary PTY arrival; only native regression /
|
|
2276
|
+
// unavailability unlocks. This is the plipping fix.
|
|
2277
|
+
// - 6 trigger strings (native_history_partial / _stale /
|
|
2278
|
+
// _not_safely_mapped / _empty / _error / _unavailable)
|
|
2279
|
+
// collapse to 3 events with diagnostic causes preserved
|
|
2280
|
+
// and mapped back to legacy fallbackReason strings for
|
|
2281
|
+
// response compatibility.
|
|
2282
|
+
// - Codex live-workspace native probe and unsafe-native
|
|
2283
|
+
// daemon mirror fallbacks are preserved as additional
|
|
2284
|
+
// input rounds to the machine; they were never the source
|
|
2285
|
+
// decision itself, they were retries.
|
|
2286
|
+
// ───────────────────────────────────────────────────────────
|
|
2287
|
+
|
|
2288
|
+
const supportsNative = supportsCliNativeTranscript(providerType, provider)
|
|
2289
|
+
&& isNativeSourceCanonicalHistory(provider?.nativeHistory);
|
|
2290
|
+
const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
|
|
2291
|
+
const workspace = sessionWorkspace;
|
|
2292
|
+
const nativeHistoryLimit = Math.max(
|
|
2293
|
+
normalizeReadChatTailLimit(args) || 0,
|
|
2294
|
+
returnedMessages.length,
|
|
2295
|
+
HOT_TAIL_MIN_LIMIT,
|
|
2296
|
+
);
|
|
2297
|
+
const nativeHistorySessionId = supportsNative
|
|
2298
|
+
? resolveCliNativeHistorySessionId(args, historySessionId, providerSessionId)
|
|
2299
|
+
: undefined;
|
|
2300
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
2301
|
+
const skipLiveNativeHistoryWithoutProviderSession = shouldSkipLiveCliNativeHistoryWithoutProviderSession({
|
|
2302
|
+
adapter,
|
|
2303
|
+
providerType,
|
|
2304
|
+
readChatArgs: args,
|
|
2305
|
+
nativeHistorySessionId,
|
|
2306
|
+
parsedProviderSessionId: providerSessionId,
|
|
2307
|
+
});
|
|
2308
|
+
const nativeHistoryReadSessionId = skipLiveNativeHistoryWithoutProviderSession
|
|
2309
|
+
? undefined
|
|
2310
|
+
: nativeHistorySessionId;
|
|
2311
|
+
const exactNativeHistoryScope = Boolean(
|
|
2312
|
+
(typeof args?.historySessionId === 'string' && args.historySessionId.trim())
|
|
2313
|
+
|| (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
|
|
2314
|
+
|| providerSessionId
|
|
2315
|
+
|| (nativeHistoryReadSessionId && nativeHistoryReadSessionId !== targetSessionId)
|
|
2316
|
+
|| ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
|
|
2317
|
+
);
|
|
2318
|
+
|
|
2319
|
+
// 1. Fetch native history (or skip if provider does not support it).
|
|
2320
|
+
let nativeHistory: any | null = null;
|
|
2321
|
+
let nativeHistoryError: unknown | undefined;
|
|
2322
|
+
if (supportsNative) {
|
|
2323
|
+
try {
|
|
2324
|
+
nativeHistory = readCliProviderNativeHistory(agentStr, {
|
|
2325
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2326
|
+
historySessionId: nativeHistoryReadSessionId,
|
|
2327
|
+
workspace,
|
|
2328
|
+
offset: 0,
|
|
2329
|
+
limit: nativeHistoryLimit,
|
|
2330
|
+
excludeRecentCount: 0,
|
|
2331
|
+
historyBehavior: provider?.historyBehavior,
|
|
2332
|
+
scripts: provider?.scripts as any,
|
|
2333
|
+
excludeInProgressTurn: returnedStatus === 'waiting_approval',
|
|
2334
|
+
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
2335
|
+
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
2336
|
+
});
|
|
2337
|
+
} catch (error: any) {
|
|
2338
|
+
nativeHistoryError = error;
|
|
2339
|
+
nativeHistory = null;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// 2. Compute safeMapping with the same rules the v1 code used so the
|
|
2344
|
+
// machine sees the same observation it always would have.
|
|
2345
|
+
let nativeMessages: ChatMessage[] = nativeHistory && Array.isArray(nativeHistory.messages)
|
|
2346
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
|
|
2347
|
+
: [];
|
|
2348
|
+
const sessionStartedAtMs = sessionStartedAtMsFromRegistry(h, args?.targetSessionId);
|
|
2349
|
+
let historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
|
|
2350
|
+
? nativeHistory.providerSessionId
|
|
2351
|
+
: readHistorySessionIdFromMessages(nativeMessages) || nativeHistoryReadSessionId || historySessionId;
|
|
2352
|
+
let lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
|
|
2353
|
+
let nativeHistorySessionForMapping = adapter.cliType === 'antigravity-cli'
|
|
2354
|
+
&& historyProviderSessionId
|
|
2355
|
+
&& nativeHistoryReadSessionId
|
|
2356
|
+
&& historyProviderSessionId !== nativeHistoryReadSessionId
|
|
2357
|
+
? undefined
|
|
2358
|
+
: nativeHistoryReadSessionId;
|
|
2359
|
+
let safeMapping = supportsNative && nativeHistory
|
|
2360
|
+
? hasSafeNativeHistoryMapping({
|
|
2361
|
+
historySessionId: lookup === 'workspace' ? undefined : nativeHistorySessionForMapping,
|
|
2362
|
+
providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId,
|
|
2363
|
+
workspace,
|
|
2364
|
+
nativeMessages,
|
|
2365
|
+
ptyMessages: returnedMessages,
|
|
2366
|
+
requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
|
|
2367
|
+
})
|
|
2368
|
+
: false;
|
|
2369
|
+
if (skipLiveNativeHistoryWithoutProviderSession && (!safeMapping || returnedMessages.length === 0)) {
|
|
2370
|
+
nativeHistory = null;
|
|
2371
|
+
nativeMessages = [];
|
|
2372
|
+
historyProviderSessionId = undefined;
|
|
2373
|
+
lookup = 'session';
|
|
2374
|
+
safeMapping = false;
|
|
2375
|
+
}
|
|
2376
|
+
const mayRetryUnsafeAutoDetectedCodexSession = adapter.cliType === 'codex-cli'
|
|
2377
|
+
&& !getExplicitHistorySessionId(args)
|
|
2378
|
+
&& Boolean(sessionStartedAtMs && sessionStartedAtMs > 0)
|
|
2379
|
+
&& !skipLiveNativeHistoryWithoutProviderSession
|
|
2380
|
+
&& !safeMapping;
|
|
2381
|
+
if (mayRetryUnsafeAutoDetectedCodexSession) {
|
|
2382
|
+
try {
|
|
2383
|
+
nativeHistory = readCliProviderNativeHistory(agentStr, {
|
|
2384
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2385
|
+
historySessionId: undefined,
|
|
2386
|
+
workspace,
|
|
2387
|
+
offset: 0,
|
|
2388
|
+
limit: nativeHistoryLimit,
|
|
2389
|
+
excludeRecentCount: 0,
|
|
2390
|
+
historyBehavior: provider?.historyBehavior,
|
|
2391
|
+
scripts: provider?.scripts as any,
|
|
2392
|
+
excludeInProgressTurn: returnedStatus === 'waiting_approval',
|
|
2393
|
+
sessionStartedAtMs,
|
|
2394
|
+
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
2395
|
+
});
|
|
2396
|
+
nativeHistoryError = undefined;
|
|
2397
|
+
nativeMessages = nativeHistory && Array.isArray(nativeHistory.messages)
|
|
2398
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
|
|
2399
|
+
: [];
|
|
2400
|
+
historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
|
|
2401
|
+
? nativeHistory.providerSessionId
|
|
2402
|
+
: readHistorySessionIdFromMessages(nativeMessages);
|
|
2403
|
+
lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
|
|
2404
|
+
nativeHistorySessionForMapping = undefined;
|
|
2405
|
+
safeMapping = supportsNative && nativeHistory
|
|
2406
|
+
? hasSafeNativeHistoryMapping({
|
|
2407
|
+
historySessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
|
|
2408
|
+
providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
|
|
2409
|
+
workspace,
|
|
2410
|
+
nativeMessages,
|
|
2411
|
+
ptyMessages: returnedMessages,
|
|
2412
|
+
requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
|
|
2413
|
+
})
|
|
2414
|
+
: false;
|
|
2415
|
+
} catch (error: any) {
|
|
2416
|
+
nativeHistoryError = error;
|
|
2417
|
+
nativeHistory = null;
|
|
2418
|
+
nativeMessages = [];
|
|
2419
|
+
historyProviderSessionId = undefined;
|
|
2420
|
+
safeMapping = false;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
const trustedExactNativeIdentity = lookup !== 'workspace'
|
|
2424
|
+
&& Boolean(nativeHistoryReadSessionId)
|
|
2425
|
+
&& Boolean(historyProviderSessionId)
|
|
2426
|
+
&& nativeHistoryReadSessionId === historyProviderSessionId;
|
|
2427
|
+
|
|
2428
|
+
// 3. Drive ChatSourceMachine — one observation per readChat call,
|
|
2429
|
+
// keyed by (providerType, sessionKey-for-this-call). targetSessionId
|
|
2430
|
+
// is the most specific session anchor we have; fall back to
|
|
2431
|
+
// historySessionId so we never leak state across distinct sessions.
|
|
2432
|
+
const machineSessionKey = String(
|
|
2433
|
+
args?.targetSessionId
|
|
2434
|
+
|| providerSessionId
|
|
2435
|
+
|| historySessionId
|
|
2436
|
+
|| (h.currentSession as any)?.sessionId
|
|
2437
|
+
|| ''
|
|
2438
|
+
);
|
|
2439
|
+
const primary = decideCliReadChatSource({
|
|
2440
|
+
providerType,
|
|
2441
|
+
provider,
|
|
2442
|
+
sessionId: machineSessionKey,
|
|
2443
|
+
nativeHistoryResult: nativeHistory,
|
|
2444
|
+
nativeHistoryError,
|
|
2445
|
+
safeMapping,
|
|
2446
|
+
trustedExactNativeIdentity,
|
|
2447
|
+
sessionWorkspace,
|
|
2448
|
+
intendedWorkspace,
|
|
2449
|
+
ptyMessages: returnedMessages,
|
|
2450
|
+
// Start with PTY visible; decideCliReadChatSource flips this
|
|
2451
|
+
// to true when the machine actually selects native-history.
|
|
2452
|
+
ptyStatusApprovalOnly: false,
|
|
2453
|
+
});
|
|
2454
|
+
let messageSource: Record<string, unknown> = primary.messageSource;
|
|
2455
|
+
|
|
2456
|
+
if (primary.nativeSelected) {
|
|
2457
|
+
selectedMessages = finalizeStreamingMessagesWhenIdle(primary.nativeMessages, returnedStatus);
|
|
2458
|
+
selectedProviderSessionId = historyProviderSessionId || providerSessionId;
|
|
2459
|
+
selectedTranscriptAuthority = 'provider';
|
|
2460
|
+
selectedCoverage = nativeHistory?.hasMore ? 'tail' : 'full';
|
|
2461
|
+
if (selectedProviderSessionId && selectedProviderSessionId !== providerSessionId) {
|
|
2462
|
+
adapter.updateRuntimeMeta?.({ providerSessionId: selectedProviderSessionId });
|
|
2463
|
+
}
|
|
2464
|
+
} else if (supportsNative) {
|
|
2465
|
+
// Native not selected. Two preserved v1 fallbacks before settling
|
|
2466
|
+
// on PTY: (a) Codex-only live workspace native probe; (b) unsafe-
|
|
2467
|
+
// native daemon mirror selection. The machine sees each retry as
|
|
2468
|
+
// an additional observation.
|
|
2469
|
+
const liveCurrentRuntimePtySafe = isCurrentRuntimePtySafelyAttributed({
|
|
2470
|
+
adapter,
|
|
2471
|
+
helpers: h,
|
|
2472
|
+
readChatArgs: args,
|
|
2473
|
+
sessionWorkspace,
|
|
2474
|
+
intendedWorkspace,
|
|
2475
|
+
ptyMessages: returnedMessages,
|
|
2476
|
+
});
|
|
2477
|
+
const mayProbeLiveCodexWorkspaceNative = adapter.cliType === 'codex-cli'
|
|
2478
|
+
&& liveCurrentRuntimePtySafe
|
|
2479
|
+
&& !(typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
|
|
2480
|
+
&& !(providerSessionId && providerSessionId.trim())
|
|
2481
|
+
&& !nativeHistoryReadSessionId
|
|
2482
|
+
&& (!historyProviderSessionId || historyProviderSessionId === nativeHistoryReadSessionId || historyProviderSessionId === historySessionId)
|
|
2483
|
+
&& !skipLiveNativeHistoryWithoutProviderSession;
|
|
2484
|
+
const liveWorkspaceNativeHistory = mayProbeLiveCodexWorkspaceNative
|
|
2485
|
+
? readLiveCodexWorkspaceNativeHistory(agentStr, {
|
|
2486
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2487
|
+
workspace,
|
|
2488
|
+
offset: 0,
|
|
2489
|
+
limit: nativeHistoryLimit,
|
|
2490
|
+
excludeRecentCount: 0,
|
|
2491
|
+
historyBehavior: provider?.historyBehavior,
|
|
2492
|
+
scripts: provider?.scripts as any,
|
|
2493
|
+
})
|
|
2494
|
+
: null;
|
|
2495
|
+
const liveWorkspaceNativeMessages = Array.isArray((liveWorkspaceNativeHistory as any)?.messages)
|
|
2496
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, (liveWorkspaceNativeHistory as any).messages as ChatMessage[], (liveWorkspaceNativeHistory as any)?.providerSessionId)
|
|
2497
|
+
: [];
|
|
2498
|
+
const liveWorkspaceNativeProviderSessionId = typeof (liveWorkspaceNativeHistory as any)?.providerSessionId === 'string'
|
|
2499
|
+
? (liveWorkspaceNativeHistory as any).providerSessionId
|
|
2500
|
+
: readHistorySessionIdFromMessages(liveWorkspaceNativeMessages);
|
|
2501
|
+
const liveWorkspaceNativeSafeMapping = liveWorkspaceNativeMessages.length > 0
|
|
2502
|
+
&& hasSafeNativeHistoryMapping({
|
|
2503
|
+
workspace,
|
|
2504
|
+
nativeMessages: liveWorkspaceNativeMessages,
|
|
2505
|
+
ptyMessages: returnedMessages,
|
|
2506
|
+
requireWorkspaceContentOverlap: true,
|
|
2507
|
+
});
|
|
2508
|
+
if (liveWorkspaceNativeHistory) {
|
|
2509
|
+
const liveDecision = decideCliReadChatSource({
|
|
2510
|
+
providerType,
|
|
2511
|
+
provider,
|
|
2512
|
+
// Distinct session key so a transient codex live-probe does not
|
|
2513
|
+
// clobber the primary session's lock. The machine treats this
|
|
2514
|
+
// as its own session; the primary session's state is untouched.
|
|
2515
|
+
sessionId: `${machineSessionKey}::live-workspace`,
|
|
2516
|
+
nativeHistoryResult: liveWorkspaceNativeHistory,
|
|
2517
|
+
safeMapping: liveWorkspaceNativeSafeMapping,
|
|
2518
|
+
sessionWorkspace,
|
|
2519
|
+
intendedWorkspace,
|
|
2520
|
+
ptyMessages: returnedMessages,
|
|
2521
|
+
ptyStatusApprovalOnly: true,
|
|
2522
|
+
});
|
|
2523
|
+
if (liveDecision.nativeSelected) {
|
|
2524
|
+
selectedMessages = finalizeStreamingMessagesWhenIdle(liveDecision.nativeMessages, returnedStatus);
|
|
2525
|
+
selectedProviderSessionId = liveWorkspaceNativeProviderSessionId || providerSessionId;
|
|
2526
|
+
selectedTranscriptAuthority = 'provider';
|
|
2527
|
+
selectedCoverage = (liveWorkspaceNativeHistory as any).hasMore ? 'tail' : 'full';
|
|
2528
|
+
if (selectedProviderSessionId && selectedProviderSessionId !== providerSessionId) {
|
|
2529
|
+
adapter.updateRuntimeMeta?.({ providerSessionId: selectedProviderSessionId });
|
|
2530
|
+
}
|
|
2531
|
+
messageSource = liveDecision.messageSource;
|
|
2532
|
+
(messageSource as any).selectedDaemonSource = 'live-workspace-native-history';
|
|
2533
|
+
(messageSource as any).runtimeMappingSafe = true;
|
|
2534
|
+
} else {
|
|
2535
|
+
// Live probe also rejected: apply unsafe-native daemon mirror
|
|
2536
|
+
// selection (codex-only) using the primary decision's
|
|
2537
|
+
// fallbackReason.
|
|
2538
|
+
applyUnsafeNativeDaemonFallback({
|
|
2539
|
+
providerType,
|
|
2540
|
+
adapter,
|
|
2541
|
+
helpers: h,
|
|
2542
|
+
readChatArgs: args,
|
|
2543
|
+
sessionWorkspace,
|
|
2544
|
+
intendedWorkspace,
|
|
2545
|
+
ptyMessages: returnedMessages,
|
|
2546
|
+
nativeHistoryLimit,
|
|
2547
|
+
provider,
|
|
2548
|
+
messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
|
|
2549
|
+
apply(selection) {
|
|
2550
|
+
selectedMessages = selection.messages;
|
|
2551
|
+
selectedTranscriptAuthority = selection.transcriptAuthority;
|
|
2552
|
+
selectedCoverage = selection.coverage ?? coverage;
|
|
2553
|
+
selectedStatus = selection.status ?? returnedStatus;
|
|
2554
|
+
},
|
|
2555
|
+
activeModal,
|
|
2556
|
+
returnedStatus,
|
|
2557
|
+
coverage,
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
} else {
|
|
2561
|
+
applyUnsafeNativeDaemonFallback({
|
|
2562
|
+
providerType,
|
|
2563
|
+
adapter,
|
|
2564
|
+
helpers: h,
|
|
2565
|
+
readChatArgs: args,
|
|
2566
|
+
sessionWorkspace,
|
|
2567
|
+
intendedWorkspace,
|
|
2568
|
+
ptyMessages: returnedMessages,
|
|
2569
|
+
nativeHistoryLimit,
|
|
2570
|
+
provider,
|
|
2571
|
+
messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
|
|
2572
|
+
apply(selection) {
|
|
2573
|
+
selectedMessages = selection.messages;
|
|
2574
|
+
selectedTranscriptAuthority = selection.transcriptAuthority;
|
|
2575
|
+
selectedCoverage = selection.coverage ?? coverage;
|
|
2576
|
+
selectedStatus = selection.status ?? returnedStatus;
|
|
2577
|
+
},
|
|
2578
|
+
activeModal,
|
|
2579
|
+
returnedStatus,
|
|
2580
|
+
coverage,
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (
|
|
2585
|
+
isGeneratingLikeStatus(selectedStatus)
|
|
2586
|
+
&& selectedTranscriptAuthority === 'provider'
|
|
2587
|
+
&& !hasNonEmptyModalButtons(activeModal)
|
|
2588
|
+
&& hasFinalVisibleAssistantMessage(selectedMessages)
|
|
2589
|
+
) {
|
|
2590
|
+
selectedStatus = 'idle';
|
|
2591
|
+
selectedMessages = finalizeStreamingMessagesWhenIdle(selectedMessages, selectedStatus);
|
|
2592
|
+
messageSource = {
|
|
2593
|
+
...messageSource,
|
|
2594
|
+
statusReconciled: {
|
|
2595
|
+
from: returnedStatus,
|
|
2596
|
+
to: 'idle',
|
|
2597
|
+
reason: 'provider_native_final_assistant',
|
|
2598
|
+
},
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
885
2601
|
LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length} returnedMsgCount=${returnedMessages.length}`);
|
|
886
2602
|
return buildReadChatCommandResult({
|
|
887
|
-
messages:
|
|
888
|
-
status:
|
|
2603
|
+
messages: selectedMessages,
|
|
2604
|
+
status: selectedStatus,
|
|
889
2605
|
activeModal,
|
|
2606
|
+
messageSource,
|
|
2607
|
+
transcriptProvenance: messageSource,
|
|
890
2608
|
debugReadChat: {
|
|
891
2609
|
provider: adapter.cliType,
|
|
892
2610
|
targetSessionId: String(args?.targetSessionId || ''),
|
|
893
2611
|
adapterStatus: String(adapterStatus.status || ''),
|
|
894
2612
|
parsedStatus: String(parsedRecord.status || ''),
|
|
895
|
-
returnedStatus: String(
|
|
896
|
-
|
|
2613
|
+
returnedStatus: String(selectedStatus || ''),
|
|
2614
|
+
selectedMessageSource: (messageSource as any).selected,
|
|
2615
|
+
messageSource,
|
|
2616
|
+
shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType, provider)
|
|
2617
|
+
&& isNativeSourceCanonicalHistory(provider?.nativeHistory)
|
|
2618
|
+
&& (messageSource as any).selected !== 'native-history'
|
|
2619
|
+
&& typeof (messageSource as any).fallbackReason === 'string'
|
|
2620
|
+
&& (messageSource as any).fallbackReason.startsWith('native_history_')
|
|
2621
|
+
&& (messageSource as any).fallbackReason !== 'native_history_not_checked'
|
|
2622
|
+
&& !isUnsafeNativeTranscriptFallback((messageSource as any).fallbackReason)
|
|
2623
|
+
&& !(selectedTranscriptAuthority === 'provider' && selectedCoverage === 'full'),
|
|
897
2624
|
parsedMsgCount: parsedRecord.messages.length,
|
|
898
|
-
returnedMsgCount:
|
|
2625
|
+
returnedMsgCount: selectedMessages.length,
|
|
899
2626
|
},
|
|
900
|
-
...(
|
|
901
|
-
...(
|
|
902
|
-
...(
|
|
903
|
-
...(
|
|
904
|
-
}, args);
|
|
2627
|
+
...(selectedTitle ? { title: selectedTitle } : {}),
|
|
2628
|
+
...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
|
|
2629
|
+
...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
|
|
2630
|
+
...(selectedCoverage ? { coverage: selectedCoverage } : {}),
|
|
2631
|
+
}, args, h);
|
|
905
2632
|
}
|
|
2633
|
+
// History-only path (no adapter). Same source-decision contract as
|
|
2634
|
+
// the adapter path above, but with no PTY messages — the machine
|
|
2635
|
+
// simply decides whether native is usable; if not we return the
|
|
2636
|
+
// history we have plus a `native_history_not_safely_available`
|
|
2637
|
+
// error response when the provider requires native source.
|
|
906
2638
|
const historyLimit = normalizeReadChatTailLimit(args);
|
|
907
2639
|
try {
|
|
908
2640
|
const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h);
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
2641
|
+
const targetSid = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
2642
|
+
const registrySessionWorkspace = targetSid
|
|
2643
|
+
? (h.ctx?.sessionRegistry?.get?.(targetSid) as any)?.workspace
|
|
2644
|
+
: undefined;
|
|
2645
|
+
const currentSessionWorkspace = typeof (h.currentSession as any)?.workspace === 'string'
|
|
2646
|
+
? (h.currentSession as any).workspace
|
|
2647
|
+
: undefined;
|
|
2648
|
+
const argsWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
|
|
2649
|
+
// When reading a different session (targetSid), prefer that session's registered
|
|
2650
|
+
// workspace (or the caller-supplied args.workspace) over the current (coordinator)
|
|
2651
|
+
// session's workspace — otherwise the coordinator's cwd shadows the worker's cwd
|
|
2652
|
+
// and history lookups find the wrong files.
|
|
2653
|
+
const workspace = targetSid
|
|
2654
|
+
? (typeof registrySessionWorkspace === 'string' ? registrySessionWorkspace : argsWorkspace ?? currentSessionWorkspace)
|
|
2655
|
+
: (typeof currentSessionWorkspace === 'string' ? currentSessionWorkspace : undefined);
|
|
2656
|
+
const intendedWorkspace = argsWorkspace;
|
|
2657
|
+
const supportsNative = supportsCliNativeTranscript(agentStr, provider)
|
|
2658
|
+
&& isNativeSourceCanonicalHistory(provider?.nativeHistory);
|
|
2659
|
+
const history = supportsNative
|
|
2660
|
+
? readCliProviderNativeHistory(agentStr, {
|
|
2661
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2662
|
+
historySessionId,
|
|
2663
|
+
workspace,
|
|
2664
|
+
offset: 0,
|
|
2665
|
+
limit: historyLimit,
|
|
2666
|
+
excludeRecentCount: 0,
|
|
2667
|
+
historyBehavior: provider?.historyBehavior,
|
|
2668
|
+
scripts: provider?.scripts as any,
|
|
2669
|
+
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
2670
|
+
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
2671
|
+
})
|
|
2672
|
+
: readProviderChatHistory(agentStr, {
|
|
2673
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2674
|
+
historySessionId,
|
|
2675
|
+
workspace,
|
|
2676
|
+
offset: 0,
|
|
2677
|
+
limit: historyLimit,
|
|
2678
|
+
excludeRecentCount: 0,
|
|
2679
|
+
historyBehavior: provider?.historyBehavior,
|
|
2680
|
+
scripts: provider?.scripts as any,
|
|
2681
|
+
});
|
|
2682
|
+
const lookup = (history as any)?.lookup === 'workspace' ? 'workspace' : 'session';
|
|
2683
|
+
const historyMessages = Array.isArray((history as any)?.messages)
|
|
2684
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, (history as any).messages as ChatMessage[], (history as any)?.providerSessionId)
|
|
2685
|
+
: [];
|
|
924
2686
|
const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
|
|
925
2687
|
? (history as any).providerSessionId
|
|
926
|
-
: historySessionId;
|
|
2688
|
+
: readHistorySessionIdFromMessages(historyMessages) || historySessionId;
|
|
2689
|
+
const safeMapping = supportsNative
|
|
2690
|
+
? hasSafeNativeHistoryMapping({
|
|
2691
|
+
historySessionId: lookup === 'workspace' ? undefined : historySessionId,
|
|
2692
|
+
providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
|
|
2693
|
+
workspace,
|
|
2694
|
+
nativeMessages: historyMessages,
|
|
2695
|
+
})
|
|
2696
|
+
: false;
|
|
2697
|
+
const trustedExactNativeIdentity = lookup !== 'workspace'
|
|
2698
|
+
&& Boolean(historySessionId)
|
|
2699
|
+
&& Boolean(historyProviderSessionId)
|
|
2700
|
+
&& historySessionId === historyProviderSessionId;
|
|
2701
|
+
|
|
2702
|
+
const machineSessionKey = String(
|
|
2703
|
+
args?.targetSessionId
|
|
2704
|
+
|| historyProviderSessionId
|
|
2705
|
+
|| historySessionId
|
|
2706
|
+
|| (h.currentSession as any)?.sessionId
|
|
2707
|
+
|| ''
|
|
2708
|
+
);
|
|
2709
|
+
const decision = decideCliReadChatSource({
|
|
2710
|
+
providerType: agentStr,
|
|
2711
|
+
provider,
|
|
2712
|
+
sessionId: machineSessionKey,
|
|
2713
|
+
nativeHistoryResult: history,
|
|
2714
|
+
safeMapping,
|
|
2715
|
+
trustedExactNativeIdentity,
|
|
2716
|
+
sessionWorkspace: workspace,
|
|
2717
|
+
intendedWorkspace,
|
|
2718
|
+
ptyMessages: [],
|
|
2719
|
+
ptyStatusApprovalOnly: false,
|
|
2720
|
+
});
|
|
2721
|
+
|
|
2722
|
+
if (supportsNative && !decision.nativeSelected) {
|
|
2723
|
+
// Dead-end: we are in the history-only path (no live PTY/ACP
|
|
2724
|
+
// adapter was found for this target session) AND provider-native
|
|
2725
|
+
// history is not safely mappable to the requested session
|
|
2726
|
+
// (no historySessionId stamp / workspace mismatch). Previously
|
|
2727
|
+
// this returned `success:false`, which the command logger emits
|
|
2728
|
+
// at warn level on EVERY poll (handler.ts logCommandEnd) —
|
|
2729
|
+
// mesh coordinators poll read_chat continuously, so a worker whose
|
|
2730
|
+
// transcript can never be safely mapped produced a 100% warn-log
|
|
2731
|
+
// storm with no recovery. Switch to a SOFT response: success with
|
|
2732
|
+
// empty messages + pending:true so the coordinator treats it as
|
|
2733
|
+
// "no live messages readable yet" rather than a hard failure, and
|
|
2734
|
+
// carry the machine-readable reason for debuggability. The normal
|
|
2735
|
+
// live-adapter path (above) and the safe-native return (below) are
|
|
2736
|
+
// unaffected — this is strictly the both-absent dead end.
|
|
2737
|
+
LOG.debug('Command', `[read_chat] soft pending: no live adapter and native history not safely mappable target=${String(args?.targetSessionId || '')} provider=${agentStr} reason=native_history_not_safely_available`);
|
|
2738
|
+
return {
|
|
2739
|
+
success: true,
|
|
2740
|
+
pending: true,
|
|
2741
|
+
// Both signals are true here: we reached the history-only path
|
|
2742
|
+
// because no live adapter was found (`live_adapter_not_found`),
|
|
2743
|
+
// and native history is not safely mappable
|
|
2744
|
+
// (`native_history_not_safely_available`).
|
|
2745
|
+
reason: 'native_history_not_safely_available',
|
|
2746
|
+
reasons: ['live_adapter_not_found', 'native_history_not_safely_available'],
|
|
2747
|
+
code: 'native_history_not_safely_available',
|
|
2748
|
+
messages: [],
|
|
2749
|
+
status: 'idle',
|
|
2750
|
+
providerSessionId: historyProviderSessionId,
|
|
2751
|
+
messageSource: decision.messageSource,
|
|
2752
|
+
transcriptProvenance: decision.messageSource,
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
927
2755
|
return buildReadChatCommandResult({
|
|
928
|
-
messages:
|
|
2756
|
+
messages: historyMessages,
|
|
929
2757
|
status: 'idle',
|
|
2758
|
+
messageSource: decision.messageSource,
|
|
2759
|
+
transcriptProvenance: decision.messageSource,
|
|
930
2760
|
...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
|
|
931
2761
|
...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
|
|
932
2762
|
...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')
|
|
933
2763
|
? { transcriptAuthority: (provider?.historyBehavior as any).transcriptAuthority }
|
|
934
2764
|
: {}),
|
|
935
2765
|
coverage: 'tail',
|
|
936
|
-
}, args);
|
|
2766
|
+
}, args, h);
|
|
937
2767
|
} catch (error: any) {
|
|
938
2768
|
return { success: false, error: error?.message || `${transport} adapter not found` };
|
|
939
2769
|
}
|
|
@@ -973,7 +2803,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
973
2803
|
args?.targetSessionId,
|
|
974
2804
|
historySessionId,
|
|
975
2805
|
);
|
|
976
|
-
return buildReadChatCommandResult(validated as Record<string, any>, args);
|
|
2806
|
+
return buildReadChatCommandResult(validated as Record<string, any>, args, h);
|
|
977
2807
|
}
|
|
978
2808
|
if (!extensionReadChatError) {
|
|
979
2809
|
extensionReadChatError = 'extension read_chat returned a non-object payload';
|
|
@@ -1012,7 +2842,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1012
2842
|
messages: stream.messages || [],
|
|
1013
2843
|
status: stream.status,
|
|
1014
2844
|
agentType: stream.agentType,
|
|
1015
|
-
}, args);
|
|
2845
|
+
}, args, h);
|
|
1016
2846
|
}
|
|
1017
2847
|
}
|
|
1018
2848
|
}
|
|
@@ -1052,7 +2882,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1052
2882
|
args?.targetSessionId,
|
|
1053
2883
|
historySessionId,
|
|
1054
2884
|
);
|
|
1055
|
-
return buildReadChatCommandResult(validated as Record<string, any>, args);
|
|
2885
|
+
return buildReadChatCommandResult(validated as Record<string, any>, args, h);
|
|
1056
2886
|
}
|
|
1057
2887
|
if (!webviewReadChatError) {
|
|
1058
2888
|
webviewReadChatError = 'webview read_chat returned a non-object payload';
|
|
@@ -1102,7 +2932,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1102
2932
|
args?.targetSessionId,
|
|
1103
2933
|
historySessionId,
|
|
1104
2934
|
);
|
|
1105
|
-
return buildReadChatCommandResult(validated as Record<string, any>, args);
|
|
2935
|
+
return buildReadChatCommandResult(validated as Record<string, any>, args, h);
|
|
1106
2936
|
}
|
|
1107
2937
|
if (!ideReadChatError) {
|
|
1108
2938
|
ideReadChatError = 'ide read_chat returned a non-object payload';
|
|
@@ -1184,8 +3014,24 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1184
3014
|
assertTextOnlyInput(provider, input);
|
|
1185
3015
|
if (!text) return { success: false, error: 'text required for PTY send' };
|
|
1186
3016
|
await waitOnceForFreshHermesCliStart(adapter, _log);
|
|
1187
|
-
|
|
1188
|
-
|
|
3017
|
+
const forceSend = args?.force === true || args?.forceSend === true;
|
|
3018
|
+
if (forceSend && typeof adapter.forceSendMessage === 'function') {
|
|
3019
|
+
await adapter.forceSendMessage(text);
|
|
3020
|
+
} else if (forceSend) {
|
|
3021
|
+
await adapter.sendMessage(text, { force: true });
|
|
3022
|
+
} else {
|
|
3023
|
+
await adapter.sendMessage(text);
|
|
3024
|
+
}
|
|
3025
|
+
const target = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
|
|
3026
|
+
if (target?.category === 'cli'
|
|
3027
|
+
&& target.type === adapter.cliType
|
|
3028
|
+
&& typeof target.recordAcknowledgedUserInput === 'function') {
|
|
3029
|
+
target.recordAcknowledgedUserInput(input);
|
|
3030
|
+
}
|
|
3031
|
+
return {
|
|
3032
|
+
..._logSendSuccess(`${transport}-adapter`, adapter.cliType),
|
|
3033
|
+
...(forceSend ? { forceSent: true } : {}),
|
|
3034
|
+
};
|
|
1189
3035
|
} catch (e: any) {
|
|
1190
3036
|
return { success: false, error: `${transport} send failed: ${e.message}` };
|
|
1191
3037
|
}
|
|
@@ -1742,9 +3588,18 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
|
|
|
1742
3588
|
if (buttonIndex < 0 && (action === 'always' || /always/i.test(button))) {
|
|
1743
3589
|
buttonIndex = buttons.findIndex(b => /always/i.test(b));
|
|
1744
3590
|
}
|
|
3591
|
+
if (buttonIndex < 0 && (action === 'approve' || action === 'accept')) {
|
|
3592
|
+
buttonIndex = pickApprovalButton(buttons, provider).index;
|
|
3593
|
+
}
|
|
1745
3594
|
if (buttonIndex < 0) {
|
|
1746
3595
|
return { success: false, error: 'Approval action did not match any visible button' };
|
|
1747
3596
|
}
|
|
3597
|
+
// Idempotency: if the adapter already resolved this approval within cooldown, report
|
|
3598
|
+
// stale_prompt rather than writing a second key to the PTY.
|
|
3599
|
+
if (typeof adapter.isApprovalRecentlyResolved === 'function' && adapter.isApprovalRecentlyResolved()) {
|
|
3600
|
+
LOG.info('Command', `[resolveAction] CLI PTY → stale_prompt (already resolved within cooldown)`);
|
|
3601
|
+
return { success: true, stalePrompt: true, buttonIndex, button: buttons[buttonIndex] ?? button };
|
|
3602
|
+
}
|
|
1748
3603
|
if (typeof adapter.resolveModal === 'function') {
|
|
1749
3604
|
adapter.resolveModal(buttonIndex);
|
|
1750
3605
|
} else {
|