@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.490
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/daemon-lifecycle.d.ts +9 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/build-info.d.ts +37 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +68 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +220 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +137 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
- package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +69 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
- package/dist/commands/chat-commands-read.d.ts +23 -0
- package/dist/commands/chat-commands-scope.d.ts +39 -0
- package/dist/commands/chat-commands-shared.d.ts +33 -0
- package/dist/commands/chat-commands-write.d.ts +14 -0
- package/dist/commands/chat-commands.d.ts +10 -23
- package/dist/commands/cli-manager.d.ts +71 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +64 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +48 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +152 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router-aggregate-status.d.ts +24 -0
- package/dist/commands/router-mesh-session-owner.d.ts +59 -0
- package/dist/commands/router-refine.d.ts +151 -0
- package/dist/commands/router-worktree-cleanup.d.ts +133 -0
- package/dist/commands/router.d.ts +203 -15
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +11 -0
- package/dist/config/config.d.ts +19 -0
- package/dist/config/mesh-config.d.ts +65 -0
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/registry-resolver.d.ts +54 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/config/state-store.d.ts +30 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/cli-detector.d.ts +17 -0
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +26 -2
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +80 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/git-worktree.d.ts +71 -1
- package/dist/git/index.d.ts +3 -1
- package/dist/index.d.ts +70 -17
- package/dist/index.js +53532 -19073
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +53283 -18989
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/debug-config.d.ts +16 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +81 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +256 -0
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +203 -0
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
- package/dist/mesh/mesh-event-classify.d.ts +7 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
- package/dist/mesh/mesh-events-pending.d.ts +258 -0
- package/dist/mesh/mesh-events-stale.d.ts +61 -0
- package/dist/mesh/mesh-events-utils.d.ts +75 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +21 -1
- package/dist/mesh/mesh-init.d.ts +128 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
- package/dist/mesh/mesh-ledger.d.ts +139 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-missions.d.ts +281 -0
- package/dist/mesh/mesh-node-identity.d.ts +310 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
- package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
- package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
- package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +457 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +567 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
- package/dist/mesh/mesh-task-inflight.d.ts +46 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +392 -1
- package/dist/mesh/preview-freshness.d.ts +28 -0
- package/dist/mesh/refine-config.d.ts +97 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +46 -0
- package/dist/providers/chat-message-normalization.d.ts +58 -1
- package/dist/providers/cli-provider-effect-format.d.ts +30 -0
- package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
- package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
- package/dist/providers/cli-provider-instance-types.d.ts +45 -0
- package/dist/providers/cli-provider-instance.d.ts +426 -30
- package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
- package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
- package/dist/providers/contracts.d.ts +196 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +79 -0
- package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/constants.d.ts +12 -0
- package/dist/providers/native-history/dispatcher.d.ts +44 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +30 -0
- package/dist/providers/provider-instance.d.ts +31 -1
- package/dist/providers/provider-loader.d.ts +42 -5
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +98 -0
- package/dist/providers/spec/cli-adapter.d.ts +223 -0
- package/dist/providers/spec/evaluator.d.ts +45 -0
- package/dist/providers/spec/fsm-driver.d.ts +449 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +202 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +244 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +616 -12
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/session-host/managed-host.d.ts +64 -0
- package/dist/session-host/runtime-surface.d.ts +10 -16
- package/dist/sessions/registry.d.ts +26 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +110 -55
- package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +27 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +20 -6
- package/package.json +11 -4
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +80 -14
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +3 -1
- package/src/cli-adapter-types.ts +68 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1282 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +1048 -1143
- package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/src/cli-adapters/provider-cli-parse.ts +19 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +121 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/pty-write-chunking.ts +106 -0
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +204 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2788 -0
- package/src/commands/chat-commands-scope.ts +54 -0
- package/src/commands/chat-commands-shared.ts +114 -0
- package/src/commands/chat-commands-write.ts +891 -0
- package/src/commands/chat-commands.ts +19 -1841
- package/src/commands/cli-manager.d.ts +2 -0
- package/src/commands/cli-manager.ts +724 -27
- package/src/commands/handler.ts +865 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
- package/src/commands/high-family/mesh-events.ts +102 -0
- package/src/commands/high-family/mesh-status.ts +857 -0
- package/src/commands/high-family/types.ts +80 -0
- package/src/commands/low-family/coordinator-prompt.ts +125 -0
- package/src/commands/low-family/daemon-lifecycle.ts +120 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +87 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +282 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/med-family/cli-agent.ts +270 -0
- package/src/commands/med-family/fast-forward.ts +230 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +37 -0
- package/src/commands/med-family/mesh-crud.ts +1268 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +167 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +153 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router-aggregate-status.ts +209 -0
- package/src/commands/router-mesh-session-owner.ts +114 -0
- package/src/commands/router-refine.ts +1794 -0
- package/src/commands/router-worktree-cleanup.ts +870 -0
- package/src/commands/router.ts +567 -4149
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +517 -24
- package/src/config/config.ts +30 -0
- package/src/config/mesh-config.ts +300 -24
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/registry-resolver.ts +100 -0
- package/src/config/repo-settings.ts +111 -0
- package/src/config/state-store.ts +55 -0
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +94 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +112 -16
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +822 -48
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +261 -4
- package/src/git/index.ts +17 -0
- package/src/index.ts +190 -13
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/command-log.ts +7 -5
- package/src/logging/debug-config.ts +25 -0
- package/src/logging/debug-trace.ts +7 -2
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +341 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +510 -0
- package/src/mesh/coordinator-prompt.ts +627 -34
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +706 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-completion-synthesis.ts +416 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +284 -0
- package/src/mesh/mesh-event-classify.ts +78 -0
- package/src/mesh/mesh-event-forwarding.ts +2030 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +32 -0
- package/src/mesh/mesh-events-pending.ts +1548 -0
- package/src/mesh/mesh-events-stale.ts +452 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +43 -1035
- package/src/mesh/mesh-fast-forward.ts +856 -0
- package/src/mesh/mesh-host-ownership.ts +41 -3
- package/src/mesh/mesh-init.ts +350 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +897 -102
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-missions.ts +695 -0
- package/src/mesh/mesh-node-identity.ts +2065 -0
- package/src/mesh/mesh-queue-assignment.ts +2498 -0
- package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
- package/src/mesh/mesh-reconcile-config.ts +66 -0
- package/src/mesh/mesh-reconcile-identity.ts +103 -0
- package/src/mesh/mesh-reconcile-loop.ts +1696 -0
- package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1792 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-remote-event-pull.ts +279 -0
- package/src/mesh/mesh-review-inbox.ts +308 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +2292 -0
- package/src/mesh/mesh-scheduling-runtime.ts +198 -0
- package/src/mesh/mesh-task-inflight.ts +70 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1489 -145
- package/src/mesh/preview-freshness.ts +163 -0
- package/src/mesh/refine-config.ts +129 -12
- package/src/mesh/worktree-bootstrap-config.ts +599 -0
- package/src/providers/acp-provider-instance.ts +43 -10
- package/src/providers/approval-utils.d.ts +5 -0
- package/src/providers/approval-utils.ts +100 -6
- package/src/providers/chat-message-normalization.ts +126 -4
- package/src/providers/cli-provider-effect-format.ts +53 -0
- package/src/providers/cli-provider-history-dedup.ts +75 -0
- package/src/providers/cli-provider-input-prompt.ts +133 -0
- package/src/providers/cli-provider-instance-types.ts +131 -0
- package/src/providers/cli-provider-instance.ts +2771 -499
- package/src/providers/cli-provider-status-helpers.ts +123 -0
- package/src/providers/cli-provider-transcript-merge.ts +114 -0
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +207 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +105 -0
- package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/constants.ts +19 -0
- package/src/providers/native-history/dispatcher.ts +617 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +71 -0
- package/src/providers/provider-instance.ts +24 -1
- package/src/providers/provider-loader.ts +700 -55
- package/src/providers/provider-schema.ts +93 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +246 -0
- package/src/providers/spec/cli-adapter.ts +1064 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1528 -0
- package/src/providers/spec/fsm-evaluator.ts +290 -0
- package/src/providers/spec/fsm-loader.ts +128 -0
- package/src/providers/spec/fsm-types.ts +301 -0
- package/src/providers/spec/native-history-executor.ts +1241 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +284 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +861 -14
- package/src/runtime-defaults.ts +39 -0
- package/src/session-host/managed-host.ts +218 -0
- package/src/session-host/runtime-surface.ts +20 -80
- package/src/sessions/registry.ts +44 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +148 -55
- package/src/status/builders.ts +69 -6
- package/src/status/chat-tail-hot-sessions.ts +117 -2
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +100 -27
- package/src/system/hash.ts +23 -0
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- package/src/types.ts +26 -6
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -0,0 +1,2788 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Commands — read side: handleReadChat, handleChatHistory and the
|
|
3
|
+
* native-history / source-resolution / normalization helpers they use.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import type { CommandResult, CommandHelpers } from './handler.js';
|
|
8
|
+
import type { CliAdapter } from '../cli-adapter-types.js';
|
|
9
|
+
import { flattenContent, type ProviderModule, type ProviderScripts } from '../providers/contracts.js';
|
|
10
|
+
import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
|
|
11
|
+
import { isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
|
|
12
|
+
import { clearPersistedProviderSessionPins, loadPersistedProviderSessionPins, recordPersistedProviderSessionPin } from '../config/state-store.js';
|
|
13
|
+
import { getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
14
|
+
import { LOG } from '../logging/logger.js';
|
|
15
|
+
import { recordDebugTrace } from '../logging/debug-trace.js';
|
|
16
|
+
import { hashSignatureParts } from '../chat/chat-signatures.js';
|
|
17
|
+
import {
|
|
18
|
+
CHAT_SOURCE_REGISTRY,
|
|
19
|
+
buildV1NativePresentObservation,
|
|
20
|
+
chatSourceSessionKey,
|
|
21
|
+
type ChatSourceDecision,
|
|
22
|
+
type ChatSourceObservation,
|
|
23
|
+
type ChatSourceTransitionCause,
|
|
24
|
+
} from '../chat/source-resolver.js';
|
|
25
|
+
import type { ChatMessage } from '../types.js';
|
|
26
|
+
import { filterUserFacingChatMessages, isActivityChatMessage, isUserFacingChatMessage, normalizeChatMessages } from '../providers/chat-message-normalization.js';
|
|
27
|
+
import {
|
|
28
|
+
READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS,
|
|
29
|
+
type RuntimeChatMessageMerger,
|
|
30
|
+
getCurrentProviderType,
|
|
31
|
+
getTargetedCliAdapter,
|
|
32
|
+
getTargetInstance,
|
|
33
|
+
getTargetTransport,
|
|
34
|
+
isCliLikeTransport,
|
|
35
|
+
isExtensionTransport,
|
|
36
|
+
parseMaybeJson,
|
|
37
|
+
} from './chat-commands-shared.js';
|
|
38
|
+
import { evaluateReadChatNodeWorkspaceScope, resolveTargetSessionActualWorkspace } from './chat-commands-scope.js';
|
|
39
|
+
|
|
40
|
+
// Minimum tail floor for hot-path history/mirror reads. The dashboard requests a
|
|
41
|
+
// bounded tail (~60); we keep a small floor so a tiny requested tailLimit still
|
|
42
|
+
// has enough surrounding context for seed/mirror dedup correctness, but it must
|
|
43
|
+
// NOT dominate the hot subscribe/poll path the way the previous 200 floor did.
|
|
44
|
+
// readChatHistory now serves this as an O(tail) bounded read, so the cost scales
|
|
45
|
+
// with this floor, not with total accumulated history.
|
|
46
|
+
const HOT_TAIL_MIN_LIMIT = 60;
|
|
47
|
+
// (A2.2) CLI_NATIVE_HISTORY_FRESH_MS removed with isNativeHistoryFreshEnough.
|
|
48
|
+
// Hardcoded native-transcript provider allow-list. Deprecated. Kept only as a
|
|
49
|
+
// last-resort fallback when ProviderModule is not yet loaded; on every hit we
|
|
50
|
+
// warn so the dependency on this set is visible. A2 deletes the set entirely
|
|
51
|
+
// and routes solely through canonicalHistory.contractVersion +
|
|
52
|
+
// isNativeSourceCanonicalHistory().
|
|
53
|
+
const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli', 'antigravity-cli']);
|
|
54
|
+
|
|
55
|
+
// Last successfully-bound provider-native session id, keyed by the mesh session
|
|
56
|
+
// id (targetSessionId) the read was scoped to. The live pin lives on the
|
|
57
|
+
// CliProviderInstance and is torn down when the turn ends; a *post-turn* read
|
|
58
|
+
// then finds historySessionId empty AND canBindFromLiveSession=false (no live
|
|
59
|
+
// spawnedAtMs), so readCliProviderNativeHistory would fail closed with
|
|
60
|
+
// native_history_workspace_only_lookup_unsafe and surface providerSessionId=null
|
|
61
|
+
// + zero rows even though the transcript is physically present in state.db.
|
|
62
|
+
// Persisting the last resolved id here lets that later read reuse the known pin
|
|
63
|
+
// and run the native query normally instead of fail-closing. Refreshed on every
|
|
64
|
+
// successful bind; never lets an empty id clear a known pin. Keyed by mesh
|
|
65
|
+
// session id so pins never alias across distinct sessions sharing a workspace.
|
|
66
|
+
//
|
|
67
|
+
// The map is ALSO mirrored to disk (state.json sessionProviderSessionPins) so a
|
|
68
|
+
// pin survives a daemon restart. Without that, an attach-restored antigravity
|
|
69
|
+
// session (spawnedAtMs=0, so no live spawn floor) that has sat idle past the
|
|
70
|
+
// native reader's recency window can no longer resolve its own conversation .db
|
|
71
|
+
// after the daemon comes back — read_chat falls to the PTY parse and the
|
|
72
|
+
// dashboard shows the user prompt with the assistant tail missing
|
|
73
|
+
// (ANTIGRAVITY-FINAL-MESSAGE-TAIL-GAP). The in-memory map stays the hot path;
|
|
74
|
+
// disk is the cold-start hydration source, read lazily on the first miss.
|
|
75
|
+
const lastBoundProviderSessionIdByMeshSession = new Map<string, string>();
|
|
76
|
+
let persistedProviderSessionPinsHydrated = false;
|
|
77
|
+
|
|
78
|
+
function hydratePersistedProviderSessionPinsOnce(): void {
|
|
79
|
+
if (persistedProviderSessionPinsHydrated) return;
|
|
80
|
+
persistedProviderSessionPinsHydrated = true;
|
|
81
|
+
try {
|
|
82
|
+
for (const [key, value] of Object.entries(loadPersistedProviderSessionPins())) {
|
|
83
|
+
// Never let a stale persisted value clobber a fresher in-memory bind
|
|
84
|
+
// recorded earlier this process lifetime.
|
|
85
|
+
if (!lastBoundProviderSessionIdByMeshSession.has(key)) {
|
|
86
|
+
lastBoundProviderSessionIdByMeshSession.set(key, value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Best-effort: a missing/corrupt state file just means no cold-start pins.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function recordBoundProviderSessionId(h: CommandHelpers, meshSessionId: string | undefined, providerSessionId: string | undefined): void {
|
|
95
|
+
const key = typeof meshSessionId === 'string' ? meshSessionId.trim() : '';
|
|
96
|
+
const value = typeof providerSessionId === 'string' ? providerSessionId.trim() : '';
|
|
97
|
+
if (!key || !value) return;
|
|
98
|
+
// SSOT: the session registry entry (keyed by sessionId == instanceId) is the
|
|
99
|
+
// authoritative sessionId → conversation-uuid record. Writing it here — the
|
|
100
|
+
// moment a native read resolves the real conversation id — makes
|
|
101
|
+
// getHistorySessionId return it directly on every subsequent read, so the
|
|
102
|
+
// conversation is exact-bound instead of re-resolved by the spawn-floor/mtime
|
|
103
|
+
// heuristic (the crosswire/theft source). The pin below stays as the durable
|
|
104
|
+
// cross-restart mirror (the registry is in-memory and cleared on restart).
|
|
105
|
+
try { h.ctx?.sessionRegistry?.setProviderSessionId?.(key, value); } catch { /* best-effort SSOT write-back */ }
|
|
106
|
+
lastBoundProviderSessionIdByMeshSession.set(key, value);
|
|
107
|
+
// Always attempt the disk mirror — recordPersistedProviderSessionPin is itself a
|
|
108
|
+
// no-op when the ON-DISK value already matches, so it does not rewrite state.json
|
|
109
|
+
// on steady re-reads, yet it still lands a pin the in-memory map already holds but
|
|
110
|
+
// disk lost (a prior write clobbered by another state-store writer, or a restart
|
|
111
|
+
// whose hydration ran before this bind). Gating on the in-memory previous value
|
|
112
|
+
// let the in-memory and on-disk pin diverge permanently, defeating the persistence.
|
|
113
|
+
try { recordPersistedProviderSessionPin(key, value); } catch { /* best-effort disk mirror */ }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getBoundProviderSessionIdPin(meshSessionId: string | undefined): string | undefined {
|
|
117
|
+
const key = typeof meshSessionId === 'string' ? meshSessionId.trim() : '';
|
|
118
|
+
if (!key) return undefined;
|
|
119
|
+
hydratePersistedProviderSessionPinsOnce();
|
|
120
|
+
const pinned = lastBoundProviderSessionIdByMeshSession.get(key);
|
|
121
|
+
return pinned && pinned.trim() ? pinned.trim() : undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Test-only: clear the in-memory read-pin map and re-arm cold-start hydration so
|
|
126
|
+
* each test starts from a clean pin state. The on-disk mirror is isolated per
|
|
127
|
+
* test process via ADHDEV_CONFIG_DIR (test/helpers/setup-env.ts); this resets the
|
|
128
|
+
* module-level cache that would otherwise leak a pin across tests sharing the
|
|
129
|
+
* worker. Not part of the runtime contract.
|
|
130
|
+
*/
|
|
131
|
+
export function __resetProviderSessionPinsForTest(): void {
|
|
132
|
+
lastBoundProviderSessionIdByMeshSession.clear();
|
|
133
|
+
persistedProviderSessionPinsHydrated = false;
|
|
134
|
+
try { clearPersistedProviderSessionPins(); } catch { /* best-effort */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Test-only: read the in-memory read-pin (the mesh-session → conversation-uuid
|
|
139
|
+
* bind recorded by recordBoundProviderSessionId and mirrored to state.json
|
|
140
|
+
* sessionProviderSessionPins). Lets the antigravity-coordinator-pin tests assert
|
|
141
|
+
* that an owner-confirmed workspace-latest read recorded the pin — and that a
|
|
142
|
+
* non-owner-confirmed read did NOT. Not part of the runtime contract.
|
|
143
|
+
*/
|
|
144
|
+
export function __getProviderSessionPinForTest(meshSessionId: string): string | undefined {
|
|
145
|
+
return getBoundProviderSessionIdPin(meshSessionId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const warnedLegacyNativeAllowlistHits = new Set<string>();
|
|
149
|
+
function warnLegacyNativeAllowlistHit(providerType: string): void {
|
|
150
|
+
if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
|
|
151
|
+
warnedLegacyNativeAllowlistHits.add(providerType);
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
153
|
+
console.warn(
|
|
154
|
+
`[chat-commands] supportsCliNativeTranscript fell back to the hardcoded `
|
|
155
|
+
+ `CLI_NATIVE_TRANSCRIPT_PROVIDERS set for "${providerType}". `
|
|
156
|
+
+ `The provider module was unavailable or did not declare canonicalHistory. `
|
|
157
|
+
+ `Set canonicalHistory.contractVersion in the provider.json to remove this dependency.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function getExplicitHistorySessionId(args: any): string | undefined {
|
|
161
|
+
const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
|
|
162
|
+
if (explicit) return explicit;
|
|
163
|
+
|
|
164
|
+
const explicitProviderSessionId = typeof args?.providerSessionId === 'string' ? args.providerSessionId.trim() : '';
|
|
165
|
+
if (explicitProviderSessionId) return explicitProviderSessionId;
|
|
166
|
+
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A native-history session id is a "runtime fallback" — the daemon's own
|
|
172
|
+
* ADHDev session id (targetSessionId) standing in for a real provider-native
|
|
173
|
+
* conversation uuid — when it exactly equals targetSessionId. For an
|
|
174
|
+
* antigravity coordinator (agy takes no --session-id, so its providerSessionId
|
|
175
|
+
* never surfaces to the web), getConversationHistorySessionId falls back to the
|
|
176
|
+
* ADHDev sessionId, and the browser then sends that runtime id back as
|
|
177
|
+
* args.historySessionId. That id is NOT the on-disk conversations/<uuid>.db
|
|
178
|
+
* name (e.g. targetSessionId 28c530af vs stamped conv uuid 07f6ed3e), so a
|
|
179
|
+
* native read keyed on it can never exact-bind — it fail-closes to pty-parser
|
|
180
|
+
* (user-echo only) AND bypasses the owner-confirmed pin/live-bind resolution
|
|
181
|
+
* (which only runs when historySessionId is empty). Detect it whether it
|
|
182
|
+
* arrived EXPLICITLY (args.historySessionId === targetSessionId, the browser's
|
|
183
|
+
* poisoned read) OR only via getHistorySessionId's internal fallback (empty
|
|
184
|
+
* args), and in both cases treat historySessionId as ABSENT so the owner-
|
|
185
|
+
* confirmed native resolution engages and returns [user, assistant, ...].
|
|
186
|
+
* A REAL, DISTINCT provider conv uuid (≠ targetSessionId) is never a runtime
|
|
187
|
+
* fallback and must still exact-bind as before.
|
|
188
|
+
*/
|
|
189
|
+
function isRuntimeFallbackHistorySessionId(
|
|
190
|
+
candidateHistorySessionId: string | undefined,
|
|
191
|
+
targetSessionId: string | undefined,
|
|
192
|
+
): boolean {
|
|
193
|
+
const target = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
|
|
194
|
+
if (!target) return false;
|
|
195
|
+
const candidate = typeof candidateHistorySessionId === 'string' ? candidateHistorySessionId.trim() : '';
|
|
196
|
+
return candidate === target;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface ResolvedNativeHistoryReadSession {
|
|
200
|
+
/**
|
|
201
|
+
* True when the candidate history id is the daemon runtime session id (==
|
|
202
|
+
* targetSessionId) standing in for a real provider-native conv uuid — reached
|
|
203
|
+
* either via getHistorySessionId's internal fallback (empty args) or because
|
|
204
|
+
* the browser explicitly echoed targetSessionId back as historySessionId (the
|
|
205
|
+
* poisoned agy-coordinator read). See isRuntimeFallbackHistorySessionId.
|
|
206
|
+
*/
|
|
207
|
+
isRuntimeFallback: boolean;
|
|
208
|
+
/** Owner-confirmed pin recorded by a prior bound read for this mesh session, if any. */
|
|
209
|
+
pinnedProviderSessionId: string | undefined;
|
|
210
|
+
/**
|
|
211
|
+
* The id to key the native read on: the pin (or undefined) when the candidate
|
|
212
|
+
* is a runtime fallback so pin / workspace-latest resolution engages, else the
|
|
213
|
+
* candidate unchanged (a real DISTINCT provider uuid still exact-binds).
|
|
214
|
+
*/
|
|
215
|
+
effectiveHistorySessionId: string | undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolve the runtime-fallback → pin substitution shared by every native-history
|
|
220
|
+
* read path (handleChatHistory, the CLI-adapter main read, and the history-only
|
|
221
|
+
* read). Each site previously inlined this same four-step computation verbatim:
|
|
222
|
+
* detect the runtime fallback (candidate === targetSessionId AND no distinct
|
|
223
|
+
* explicit id), look up the owner-confirmed pin, and drop the runtime id in favor
|
|
224
|
+
* of the pin (or undefined) so readCliProviderNativeHistory's pin / workspace-
|
|
225
|
+
* latest paths can engage instead of fail-closing to pty-parser. Extracted to a
|
|
226
|
+
* single helper so the D9 historySessionId-poison guard has one definition.
|
|
227
|
+
* Behavior is identical to the inlined blocks — same target (args.targetSessionId),
|
|
228
|
+
* same explicit-id source, same pin key (getBoundProviderSessionIdPin trims).
|
|
229
|
+
*/
|
|
230
|
+
function resolveNativeHistoryReadSession(
|
|
231
|
+
args: any,
|
|
232
|
+
candidateHistorySessionId: string | undefined,
|
|
233
|
+
): ResolvedNativeHistoryReadSession {
|
|
234
|
+
const targetSid = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
235
|
+
const explicitHistorySessionId = getExplicitHistorySessionId(args);
|
|
236
|
+
const isRuntimeFallback = Boolean(
|
|
237
|
+
targetSid
|
|
238
|
+
&& isRuntimeFallbackHistorySessionId(candidateHistorySessionId, targetSid)
|
|
239
|
+
&& (!explicitHistorySessionId
|
|
240
|
+
|| isRuntimeFallbackHistorySessionId(explicitHistorySessionId, targetSid)),
|
|
241
|
+
);
|
|
242
|
+
const pinnedProviderSessionId = getBoundProviderSessionIdPin(args?.targetSessionId);
|
|
243
|
+
const effectiveHistorySessionId = isRuntimeFallback
|
|
244
|
+
? (pinnedProviderSessionId || undefined)
|
|
245
|
+
: candidateHistorySessionId;
|
|
246
|
+
return { isRuntimeFallback, pinnedProviderSessionId, effectiveHistorySessionId };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
|
|
250
|
+
const explicit = getExplicitHistorySessionId(args);
|
|
251
|
+
if (explicit) return explicit;
|
|
252
|
+
|
|
253
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
254
|
+
if (!targetSessionId) return undefined;
|
|
255
|
+
|
|
256
|
+
const session = h.ctx.sessionRegistry?.get(targetSessionId) as any;
|
|
257
|
+
const registeredProviderSessionId = typeof session?.providerSessionId === 'string' ? session.providerSessionId.trim() : '';
|
|
258
|
+
if (registeredProviderSessionId) return registeredProviderSessionId;
|
|
259
|
+
|
|
260
|
+
const instance = getTargetInstance(h, args);
|
|
261
|
+
const state = instance?.getState?.();
|
|
262
|
+
const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
|
|
263
|
+
if (providerSessionId) return providerSessionId;
|
|
264
|
+
|
|
265
|
+
const currentSession = h.currentSession as any;
|
|
266
|
+
if (currentSession?.sessionId === targetSessionId) {
|
|
267
|
+
const currentProviderSessionId = typeof currentSession.providerSessionId === 'string'
|
|
268
|
+
? currentSession.providerSessionId.trim()
|
|
269
|
+
: '';
|
|
270
|
+
if (currentProviderSessionId) return currentProviderSessionId;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return targetSessionId;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resolveCliNativeHistorySessionId(args: any, currentHistorySessionId: string | undefined, parsedProviderSessionId: string | undefined): string | undefined {
|
|
277
|
+
const explicit = getExplicitHistorySessionId(args);
|
|
278
|
+
if (explicit) return explicit;
|
|
279
|
+
|
|
280
|
+
const parsed = typeof parsedProviderSessionId === 'string' ? parsedProviderSessionId.trim() : '';
|
|
281
|
+
const current = typeof currentHistorySessionId === 'string' ? currentHistorySessionId.trim() : '';
|
|
282
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
283
|
+
|
|
284
|
+
// getHistorySessionId falls back to the runtime session id when no native
|
|
285
|
+
// handle has been registered yet. For live CLI adapters the parser may
|
|
286
|
+
// already know the provider-native handle; prefer it over the runtime id so
|
|
287
|
+
// exact native reads do not miss the worker transcript and fall back to PTY
|
|
288
|
+
// or same-workspace history.
|
|
289
|
+
if (parsed && (!current || current === targetSessionId)) return parsed;
|
|
290
|
+
return current || parsed || undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function shouldSkipLiveCliNativeHistoryWithoutProviderSession(args: {
|
|
294
|
+
adapter?: CliAdapter | null;
|
|
295
|
+
providerType?: string;
|
|
296
|
+
readChatArgs: any;
|
|
297
|
+
nativeHistorySessionId?: string;
|
|
298
|
+
parsedProviderSessionId?: string;
|
|
299
|
+
}): boolean {
|
|
300
|
+
const explicit = getExplicitHistorySessionId(args.readChatArgs);
|
|
301
|
+
if (explicit) return false;
|
|
302
|
+
|
|
303
|
+
const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
|
|
304
|
+
? args.readChatArgs.targetSessionId.trim()
|
|
305
|
+
: '';
|
|
306
|
+
if (!targetSessionId) return false;
|
|
307
|
+
|
|
308
|
+
const resolved = typeof args.nativeHistorySessionId === 'string'
|
|
309
|
+
? args.nativeHistorySessionId.trim()
|
|
310
|
+
: '';
|
|
311
|
+
if (!resolved || resolved !== targetSessionId) return false;
|
|
312
|
+
|
|
313
|
+
const parsed = typeof args.parsedProviderSessionId === 'string'
|
|
314
|
+
? args.parsedProviderSessionId.trim()
|
|
315
|
+
: '';
|
|
316
|
+
if (parsed) return false;
|
|
317
|
+
|
|
318
|
+
const cliType = args.adapter?.cliType || args.providerType || '';
|
|
319
|
+
if (cliType !== 'codex-cli') return false;
|
|
320
|
+
|
|
321
|
+
// A live Codex session starts with only the daemon runtime UUID. That UUID
|
|
322
|
+
// is not the provider-native rollout id, so using it for native history
|
|
323
|
+
// lets the file picker fall back to the newest same-workspace transcript
|
|
324
|
+
// and makes concurrent fresh sessions all show the same old conversation.
|
|
325
|
+
return !!args.adapter;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getInteractionId(args: any): string | undefined {
|
|
329
|
+
return typeof args?._interactionId === 'string' && args._interactionId.trim()
|
|
330
|
+
? args._interactionId.trim()
|
|
331
|
+
: undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function traceProviderEvent(
|
|
335
|
+
args: any,
|
|
336
|
+
category: 'provider' | 'parser',
|
|
337
|
+
stage: string,
|
|
338
|
+
options: {
|
|
339
|
+
h: CommandHelpers;
|
|
340
|
+
provider?: ProviderModule;
|
|
341
|
+
payload?: Record<string, unknown>;
|
|
342
|
+
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
343
|
+
},
|
|
344
|
+
): void {
|
|
345
|
+
recordDebugTrace({
|
|
346
|
+
interactionId: getInteractionId(args),
|
|
347
|
+
category,
|
|
348
|
+
stage,
|
|
349
|
+
level: options.level || 'info',
|
|
350
|
+
sessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : options.h.currentSession?.sessionId,
|
|
351
|
+
providerType: options.provider?.type || options.h.currentProviderType || options.h.currentSession?.providerType,
|
|
352
|
+
payload: options.payload,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function normalizeReadChatTailLimit(args: any): number {
|
|
356
|
+
const value = Number(args?.tailLimit || 0);
|
|
357
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[] {
|
|
361
|
+
const messages = Array.isArray(payload.messages) ? payload.messages as ChatMessage[] : [];
|
|
362
|
+
return normalizeChatMessages(messages);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
|
|
366
|
+
let newest = 0;
|
|
367
|
+
for (const message of messages) {
|
|
368
|
+
const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
|
|
369
|
+
if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
|
|
370
|
+
}
|
|
371
|
+
return newest;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function readHistorySessionIdFromMessages(messages: ChatMessage[]): string | undefined {
|
|
375
|
+
for (const message of messages as Array<ChatMessage & { historySessionId?: unknown }>) {
|
|
376
|
+
const historySessionId = typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '';
|
|
377
|
+
if (historySessionId) return historySessionId;
|
|
378
|
+
}
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function shouldPreserveNativeIdentity(providerType: string, sessionId: string, message: ChatMessage): boolean {
|
|
383
|
+
const providerUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
|
|
384
|
+
const turnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
|
|
385
|
+
if (!providerUnitKey) return false;
|
|
386
|
+
// (A2.3) v2 stamped identity is producer-owned and globally stable; trust it
|
|
387
|
+
// unconditionally. Producers may omit _turnKey (the daemon recomputes it
|
|
388
|
+
// from the current ordering), so do not require turnKey for v2 messages.
|
|
389
|
+
if (providerUnitKey.startsWith('v2:') || providerUnitKey.startsWith('v2-pty:')) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
// v1 identity always required both keys to be present.
|
|
393
|
+
if (!turnKey) return false;
|
|
394
|
+
if (providerType === 'hermes-cli' && sessionId) {
|
|
395
|
+
return providerUnitKey.startsWith(`${providerType}:native:${sessionId}:`)
|
|
396
|
+
&& turnKey.startsWith(`${providerType}:native-turn:${sessionId}:`);
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Drop the synthetic "user" message some CLIs surface in their native
|
|
403
|
+
* transcript when the daemon injects a coordinator system prompt
|
|
404
|
+
* (codex puts the AGENTS.md / developer_instructions block in as
|
|
405
|
+
* role=user; agy/claude/hermes have similar artifacts). The user can
|
|
406
|
+
* opt back into seeing it via the provider setting
|
|
407
|
+
* `showCoordinatorSystemPrompt`. Default is off — the prompt is still
|
|
408
|
+
* fully visible from the chat-header ⓘ "Session info" dialog.
|
|
409
|
+
*
|
|
410
|
+
* Matching rules:
|
|
411
|
+
* 1. Setting must be off (default).
|
|
412
|
+
* 2. There must be a registered coordinator entry for the session.
|
|
413
|
+
* 3. The candidate message is filtered when its role is user OR
|
|
414
|
+
* system and its content either contains the prompt body verbatim,
|
|
415
|
+
* OR contains the well-known coordinator marker
|
|
416
|
+
* `adhdev-mesh-coordinator-prompt`. The marker covers context-file
|
|
417
|
+
* cases (agy AGENTS.md / gemini GEMINI.md) where the CLI may wrap
|
|
418
|
+
* its own preamble around our block. Verbatim-content covers
|
|
419
|
+
* codex's developer_instructions echo.
|
|
420
|
+
*
|
|
421
|
+
* Returns the messages array unchanged when none of the rules match,
|
|
422
|
+
* so this is safe to apply unconditionally to every read_chat result.
|
|
423
|
+
*/
|
|
424
|
+
function maybeHideCoordinatorPromptMessage(
|
|
425
|
+
h: CommandHelpers,
|
|
426
|
+
providerType: string,
|
|
427
|
+
sessionId: string | undefined,
|
|
428
|
+
messages: ChatMessage[],
|
|
429
|
+
): ChatMessage[] {
|
|
430
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
431
|
+
if (!sessionId) return messages;
|
|
432
|
+
const loader = h.ctx?.providerLoader;
|
|
433
|
+
if (!loader) return messages;
|
|
434
|
+
let showSetting: unknown = undefined;
|
|
435
|
+
try {
|
|
436
|
+
showSetting = (loader as any).getSettingValue?.(providerType, 'showCoordinatorSystemPrompt');
|
|
437
|
+
} catch { /* unknown setting key for this provider — fall through */ }
|
|
438
|
+
if (showSetting === true) return messages;
|
|
439
|
+
const coord = getCoordinatorForSession(sessionId);
|
|
440
|
+
if (!coord) return messages;
|
|
441
|
+
const promptBody = typeof coord.systemPrompt === 'string' ? coord.systemPrompt : '';
|
|
442
|
+
const MARKER = 'adhdev-mesh-coordinator-prompt';
|
|
443
|
+
const filtered = messages.filter(m => {
|
|
444
|
+
const role = String((m as any)?.role || '').toLowerCase();
|
|
445
|
+
if (role !== 'user' && role !== 'system') return true;
|
|
446
|
+
const content = flattenContent((m as any)?.content);
|
|
447
|
+
if (!content) return true;
|
|
448
|
+
if (content.includes(MARKER)) return false;
|
|
449
|
+
if (promptBody && content.includes(promptBody.slice(0, Math.min(400, promptBody.length)))) return false;
|
|
450
|
+
return true;
|
|
451
|
+
});
|
|
452
|
+
if (filtered.length !== messages.length) {
|
|
453
|
+
LOG.debug('ChatFilter', `[${providerType}] hid ${messages.length - filtered.length} coordinator-prompt message(s) from ${sessionId}`);
|
|
454
|
+
}
|
|
455
|
+
return filtered;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Convenience wrapper used at every native-history call site: normalize +
|
|
460
|
+
* conditionally drop the coordinator system-prompt message. Avoids
|
|
461
|
+
* duplicating the filter at four read_chat code paths.
|
|
462
|
+
*/
|
|
463
|
+
function normalizeAndFilterNativeHistory(
|
|
464
|
+
h: CommandHelpers,
|
|
465
|
+
providerType: string,
|
|
466
|
+
args: any,
|
|
467
|
+
messages: ChatMessage[],
|
|
468
|
+
nativeSessionId?: string,
|
|
469
|
+
): ChatMessage[] {
|
|
470
|
+
const normalized = normalizeNativeHistoryMessages(providerType, messages, nativeSessionId);
|
|
471
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId
|
|
472
|
+
: typeof args?.sessionId === 'string' ? args.sessionId
|
|
473
|
+
: undefined;
|
|
474
|
+
return maybeHideCoordinatorPromptMessage(h, providerType, sessionId, normalized);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizeNativeHistoryMessages(providerType: string, messages: ChatMessage[], nativeSessionId?: string): ChatMessage[] {
|
|
478
|
+
let turnIndex = 0;
|
|
479
|
+
return normalizeChatMessages(messages).map((message, index) => {
|
|
480
|
+
const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
|
|
481
|
+
const kind = typeof message.kind === 'string' && message.kind.trim() ? message.kind.trim() : (role === 'system' ? 'system' : 'standard');
|
|
482
|
+
if ((role === 'user' || role === 'human') && index > 0) turnIndex += 1;
|
|
483
|
+
const historySessionId = typeof (message as any).historySessionId === 'string'
|
|
484
|
+
? (message as any).historySessionId.trim()
|
|
485
|
+
: '';
|
|
486
|
+
const contentHash = hashSignatureParts([
|
|
487
|
+
providerType,
|
|
488
|
+
historySessionId,
|
|
489
|
+
String(message.receivedAt || message.timestamp || index),
|
|
490
|
+
role,
|
|
491
|
+
kind,
|
|
492
|
+
flattenContent(message.content),
|
|
493
|
+
]).slice(0, 12);
|
|
494
|
+
const nativeIdentitySessionId = historySessionId || (typeof nativeSessionId === 'string' ? nativeSessionId.trim() : '');
|
|
495
|
+
const preserveNativeIdentity = shouldPreserveNativeIdentity(providerType, nativeIdentitySessionId, message);
|
|
496
|
+
const existingProviderUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
|
|
497
|
+
const existingTurnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
|
|
498
|
+
const providerUnitKey = preserveNativeIdentity
|
|
499
|
+
? existingProviderUnitKey
|
|
500
|
+
: `${providerType}:native:${nativeIdentitySessionId || 'workspace'}:${index}:${role || 'message'}:${kind}:${contentHash}`;
|
|
501
|
+
const meta = message.meta && typeof message.meta === 'object' ? message.meta as Record<string, unknown> : undefined;
|
|
502
|
+
const isSystemSessionStart = role === 'system' || kind === 'system' || kind === 'session_start';
|
|
503
|
+
const isActivity = role === 'assistant' && (kind === 'tool' || kind === 'terminal' || kind === 'thought');
|
|
504
|
+
// (A2.3) sequence emit. Producer-supplied wins (v2-stamped messages
|
|
505
|
+
// bring their own monotonic sequence); otherwise derive from
|
|
506
|
+
// receivedAt/timestamp; otherwise positional. Always present on the
|
|
507
|
+
// output so consumers (ChatSourceMachine) have a stable ordering key.
|
|
508
|
+
const existingSequence = typeof (message as any).sequence === 'number'
|
|
509
|
+
&& Number.isFinite((message as any).sequence)
|
|
510
|
+
? (message as any).sequence
|
|
511
|
+
: null;
|
|
512
|
+
const tsCandidate = Number(message.receivedAt || message.timestamp || 0);
|
|
513
|
+
const sequence = existingSequence !== null
|
|
514
|
+
? existingSequence
|
|
515
|
+
: (tsCandidate > 0 ? tsCandidate : index);
|
|
516
|
+
return {
|
|
517
|
+
...message,
|
|
518
|
+
role: role === 'human' ? 'user' : (role || 'assistant'),
|
|
519
|
+
kind: isSystemSessionStart ? 'system' : kind,
|
|
520
|
+
...(nativeIdentitySessionId ? { historySessionId: nativeIdentitySessionId } : {}),
|
|
521
|
+
providerUnitKey,
|
|
522
|
+
bubbleId: typeof message.bubbleId === 'string' && message.bubbleId.trim()
|
|
523
|
+
&& preserveNativeIdentity
|
|
524
|
+
? message.bubbleId.trim()
|
|
525
|
+
: `bubble:${providerUnitKey}`,
|
|
526
|
+
sequence,
|
|
527
|
+
_turnKey: preserveNativeIdentity
|
|
528
|
+
? existingTurnKey
|
|
529
|
+
: `${providerType}:native-turn:${nativeIdentitySessionId || 'workspace'}:${turnIndex}`,
|
|
530
|
+
bubbleState: message.bubbleState || 'final',
|
|
531
|
+
...(isSystemSessionStart ? {
|
|
532
|
+
visibility: message.visibility || 'hidden',
|
|
533
|
+
transcriptVisibility: message.transcriptVisibility || 'hidden',
|
|
534
|
+
audience: message.audience || 'internal',
|
|
535
|
+
source: message.source || 'runtime_status',
|
|
536
|
+
} : isActivity ? {
|
|
537
|
+
source: message.source || (kind === 'terminal' ? 'terminal_command' : 'tool_call'),
|
|
538
|
+
meta: { ...meta, label: message.senderName || meta?.label || (kind === 'terminal' ? 'Terminal' : 'Tool') },
|
|
539
|
+
} : {
|
|
540
|
+
source: message.source || (role === 'assistant' ? 'assistant_text' : undefined),
|
|
541
|
+
}),
|
|
542
|
+
} as ChatMessage;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function buildCliMessageSourceProvenance(args: {
|
|
547
|
+
selected: 'native-history' | 'pty-parser';
|
|
548
|
+
provider: string;
|
|
549
|
+
nativeHandle?: string;
|
|
550
|
+
sessionWorkspace?: string;
|
|
551
|
+
intendedWorkspace?: string;
|
|
552
|
+
transcriptWorkspace?: string;
|
|
553
|
+
fallbackReason?: string;
|
|
554
|
+
nativeSource?: string;
|
|
555
|
+
sourcePath?: string;
|
|
556
|
+
sourceMtimeMs?: number;
|
|
557
|
+
nativeHistoryCoverage?: string;
|
|
558
|
+
partialReason?: string;
|
|
559
|
+
unavailableReason?: string;
|
|
560
|
+
nativeMessages?: ChatMessage[];
|
|
561
|
+
ptyMessages?: ChatMessage[];
|
|
562
|
+
returnedMessages?: ChatMessage[];
|
|
563
|
+
safeMapping?: boolean;
|
|
564
|
+
freshEnough?: boolean;
|
|
565
|
+
ptyStatusApprovalOnly?: boolean;
|
|
566
|
+
}): Record<string, unknown> {
|
|
567
|
+
const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
|
|
568
|
+
const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
|
|
569
|
+
const nativeMessages = args.nativeMessages || [];
|
|
570
|
+
const ptyMessages = args.ptyMessages || [];
|
|
571
|
+
const returnedMessages = args.returnedMessages || [];
|
|
572
|
+
const identityStatus = args.selected === 'native-history'
|
|
573
|
+
? 'safe'
|
|
574
|
+
: args.fallbackReason === 'native_history_not_safely_mapped'
|
|
575
|
+
? 'ambiguous_session_identity'
|
|
576
|
+
: args.fallbackReason?.startsWith('native_history_unavailable')
|
|
577
|
+
? 'transcript_unmapped'
|
|
578
|
+
: undefined;
|
|
579
|
+
return {
|
|
580
|
+
selected: args.selected,
|
|
581
|
+
provider: args.provider,
|
|
582
|
+
providerType: args.provider,
|
|
583
|
+
...(identityStatus ? { identityStatus } : {}),
|
|
584
|
+
...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
|
|
585
|
+
...(args.nativeHandle ? { nativeSessionId: args.nativeHandle } : {}),
|
|
586
|
+
...(args.sessionWorkspace ? { sessionWorkspace: args.sessionWorkspace } : {}),
|
|
587
|
+
...(args.intendedWorkspace ? { intendedWorkspace: args.intendedWorkspace } : {}),
|
|
588
|
+
...(args.transcriptWorkspace ? { transcriptWorkspace: args.transcriptWorkspace } : {}),
|
|
589
|
+
...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
|
|
590
|
+
...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
|
|
591
|
+
...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
|
|
592
|
+
...(args.nativeHistoryCoverage ? { nativeHistoryCoverage: args.nativeHistoryCoverage } : {}),
|
|
593
|
+
...(args.partialReason ? { partialReason: args.partialReason } : {}),
|
|
594
|
+
...(args.unavailableReason ? { unavailableReason: args.unavailableReason } : {}),
|
|
595
|
+
ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
|
|
596
|
+
staleness: {
|
|
597
|
+
sourceMtimeMs: sourceMtimeMs || undefined,
|
|
598
|
+
sourceMtimeAgeMs,
|
|
599
|
+
nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
|
|
600
|
+
ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
|
|
601
|
+
freshEnough: args.freshEnough === true,
|
|
602
|
+
},
|
|
603
|
+
coverage: {
|
|
604
|
+
nativeMessageCount: nativeMessages.length,
|
|
605
|
+
ptyMessageCount: ptyMessages.length,
|
|
606
|
+
returnedMessageCount: returnedMessages.length,
|
|
607
|
+
safeMapping: args.safeMapping === true,
|
|
608
|
+
// true when PTY message bodies are suppressed and must not be treated as
|
|
609
|
+
// chat content. PTY may still contribute status/approval/screen evidence.
|
|
610
|
+
ptyMessagesSuppressed: args.selected === 'native-history' || args.ptyStatusApprovalOnly === true,
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Map a ChatSourceMachine transition cause back to the v1 messageSource
|
|
617
|
+
* `fallbackReason` vocabulary so legacy consumers (web-cloud, tests, mesh
|
|
618
|
+
* debug bundles) keep parsing strings they already know. A3 replaces the
|
|
619
|
+
* caller surface with stateTransition/lockState, after which this map can be
|
|
620
|
+
* deleted.
|
|
621
|
+
*
|
|
622
|
+
* Returns undefined when the cause does not correspond to a fallback (i.e.
|
|
623
|
+
* the source is native-history and there is nothing to explain).
|
|
624
|
+
*/
|
|
625
|
+
function causeToLegacyFallbackReason(
|
|
626
|
+
cause: ChatSourceTransitionCause,
|
|
627
|
+
selected: 'native-history' | 'pty-parser',
|
|
628
|
+
extraDetail?: { unavailableReason?: string; nativeSource?: string },
|
|
629
|
+
): string | undefined {
|
|
630
|
+
if (selected === 'native-history') return undefined;
|
|
631
|
+
switch (cause) {
|
|
632
|
+
case 'initial':
|
|
633
|
+
return 'native_history_not_checked';
|
|
634
|
+
case 'native_progressed':
|
|
635
|
+
// Selected pty-parser despite a progressed observation — that
|
|
636
|
+
// means we held PtyOnly stickily (peak unmet or non-superset).
|
|
637
|
+
return 'native_history_not_selected';
|
|
638
|
+
case 'native_regressed_shrunk':
|
|
639
|
+
return 'native_history_empty';
|
|
640
|
+
case 'native_regressed_unsafe_mapping':
|
|
641
|
+
return 'native_history_not_safely_mapped';
|
|
642
|
+
case 'native_regressed_coverage_partial':
|
|
643
|
+
return 'native_history_partial';
|
|
644
|
+
case 'native_regressed_coverage_unavailable':
|
|
645
|
+
return 'native_history_unavailable';
|
|
646
|
+
case 'native_unavailable_read_error':
|
|
647
|
+
return extraDetail?.unavailableReason
|
|
648
|
+
? `native_history_unavailable:${extraDetail.unavailableReason}`
|
|
649
|
+
: 'native_history_unavailable';
|
|
650
|
+
case 'native_unavailable_provider_unsupported':
|
|
651
|
+
return 'provider_native_transcript_not_supported';
|
|
652
|
+
case 'native_unavailable_empty':
|
|
653
|
+
return 'native_history_empty';
|
|
654
|
+
case 'native_unavailable_not_native_source':
|
|
655
|
+
return extraDetail?.nativeSource
|
|
656
|
+
? `native_history_source_${extraDetail.nativeSource}`
|
|
657
|
+
: 'native_history_unavailable';
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Translate a native-history fetch result + provider/adapter context into a
|
|
663
|
+
* ChatSourceObservation and drive ChatSourceRegistry. Returns the decision
|
|
664
|
+
* together with the legacy messageSource payload so call sites can produce
|
|
665
|
+
* a v1-compatible response without duplicating the registry plumbing.
|
|
666
|
+
*
|
|
667
|
+
* This is the replacement for the 300-line if-ladder that previously lived
|
|
668
|
+
* inline in handleReadChat. It is intentionally split out for two reasons:
|
|
669
|
+
* (1) we will call it from two places (CLI adapter branch + history-only
|
|
670
|
+
* branch) instead of duplicating the ladder, (2) tests can drive it with
|
|
671
|
+
* synthetic native-history results to verify the cause→fallbackReason
|
|
672
|
+
* mapping without booting the whole readChat pipeline.
|
|
673
|
+
*/
|
|
674
|
+
function decideCliReadChatSource(args: {
|
|
675
|
+
providerType: string;
|
|
676
|
+
provider?: ProviderModule;
|
|
677
|
+
sessionId: string;
|
|
678
|
+
nativeHistoryResult: any | null;
|
|
679
|
+
nativeHistoryError?: unknown;
|
|
680
|
+
safeMapping: boolean;
|
|
681
|
+
trustedExactNativeIdentity?: boolean;
|
|
682
|
+
sessionWorkspace?: string;
|
|
683
|
+
intendedWorkspace?: string;
|
|
684
|
+
ptyMessages: ChatMessage[];
|
|
685
|
+
ptyStatusApprovalOnly: boolean;
|
|
686
|
+
}): {
|
|
687
|
+
decision: ChatSourceDecision;
|
|
688
|
+
messageSource: Record<string, unknown>;
|
|
689
|
+
nativeMessages: ChatMessage[];
|
|
690
|
+
nativeSelected: boolean;
|
|
691
|
+
} {
|
|
692
|
+
const supportsNative = supportsCliNativeTranscript(args.providerType, args.provider);
|
|
693
|
+
const observation = buildObservationForCli(args, supportsNative);
|
|
694
|
+
const sessionKey = chatSourceSessionKey(args.providerType, args.sessionId);
|
|
695
|
+
let decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
|
|
696
|
+
|
|
697
|
+
// A restored runtime can briefly expose different native slices while the
|
|
698
|
+
// provider transcript settles (for example, startup/system rows may be
|
|
699
|
+
// filtered after the first read). The source machine correctly treats a
|
|
700
|
+
// shrinking slice as regression, but an exact provider-session lookup with
|
|
701
|
+
// no PTY transcript has no safer fallback. Re-bootstrap only this proven
|
|
702
|
+
// identity so the chat does not disappear after daemon restart.
|
|
703
|
+
if (
|
|
704
|
+
decision.selected === 'pty-parser'
|
|
705
|
+
&& args.trustedExactNativeIdentity === true
|
|
706
|
+
&& args.safeMapping
|
|
707
|
+
&& args.ptyMessages.length === 0
|
|
708
|
+
&& observation.kind === 'native_present'
|
|
709
|
+
&& observation.coverage !== 'partial'
|
|
710
|
+
&& observation.messages.length > 0
|
|
711
|
+
) {
|
|
712
|
+
CHAT_SOURCE_REGISTRY.clear(sessionKey);
|
|
713
|
+
decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const nativeMessages: ChatMessage[] = observation.kind === 'native_present'
|
|
717
|
+
? extractNativeMessagesFromResult(args.providerType, args.nativeHistoryResult)
|
|
718
|
+
: [];
|
|
719
|
+
|
|
720
|
+
const nativeSource = typeof args.nativeHistoryResult?.source === 'string'
|
|
721
|
+
? args.nativeHistoryResult.source
|
|
722
|
+
: undefined;
|
|
723
|
+
const sourcePath = typeof args.nativeHistoryResult?.sourcePath === 'string'
|
|
724
|
+
? args.nativeHistoryResult.sourcePath
|
|
725
|
+
: undefined;
|
|
726
|
+
const sourceMtimeMs = typeof args.nativeHistoryResult?.sourceMtimeMs === 'number'
|
|
727
|
+
? args.nativeHistoryResult.sourceMtimeMs
|
|
728
|
+
: undefined;
|
|
729
|
+
const coverageHint = typeof args.nativeHistoryResult?.nativeHistoryCoverage === 'string'
|
|
730
|
+
? args.nativeHistoryResult.nativeHistoryCoverage
|
|
731
|
+
: undefined;
|
|
732
|
+
const partialReason = typeof args.nativeHistoryResult?.partialReason === 'string'
|
|
733
|
+
? args.nativeHistoryResult.partialReason
|
|
734
|
+
: undefined;
|
|
735
|
+
const unavailableReason = typeof args.nativeHistoryResult?.unavailableReason === 'string'
|
|
736
|
+
? args.nativeHistoryResult.unavailableReason
|
|
737
|
+
: args.nativeHistoryError
|
|
738
|
+
? `error:${(args.nativeHistoryError as any)?.message || String(args.nativeHistoryError)}`
|
|
739
|
+
: undefined;
|
|
740
|
+
const nativeHandle = typeof args.nativeHistoryResult?.providerSessionId === 'string'
|
|
741
|
+
? args.nativeHistoryResult.providerSessionId
|
|
742
|
+
: undefined;
|
|
743
|
+
const transcriptWorkspace = typeof args.nativeHistoryResult?.workspace === 'string'
|
|
744
|
+
? args.nativeHistoryResult.workspace
|
|
745
|
+
: nativeMessages.map((m: any) => typeof m?.workspace === 'string' ? m.workspace.trim() : '').find(Boolean);
|
|
746
|
+
|
|
747
|
+
const fallbackReason = causeToLegacyFallbackReason(decision.transition.cause, decision.selected, {
|
|
748
|
+
unavailableReason,
|
|
749
|
+
nativeSource: nativeSource && nativeSource !== 'provider-native' ? nativeSource : undefined,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// ptyStatusApprovalOnly: when the machine selected native-history we
|
|
753
|
+
// suppress PTY content so the dashboard does not double-show messages
|
|
754
|
+
// already in the native transcript. When the machine selected
|
|
755
|
+
// pty-parser, PTY is the authoritative source — do NOT suppress it.
|
|
756
|
+
// Callers used to hard-code this to `nativeSelected first = true` which
|
|
757
|
+
// suppressed PTY content even when native was empty/unavailable, leaving
|
|
758
|
+
// the dashboard with zero visible messages (the codex generating/waiting
|
|
759
|
+
// approval stuck state). Trust the machine here, not the caller hint.
|
|
760
|
+
const ptyStatusApprovalOnly = decision.selected === 'native-history'
|
|
761
|
+
? true
|
|
762
|
+
: args.ptyStatusApprovalOnly;
|
|
763
|
+
|
|
764
|
+
const messageSource = buildCliMessageSourceProvenance({
|
|
765
|
+
selected: decision.selected,
|
|
766
|
+
provider: args.providerType,
|
|
767
|
+
nativeHandle,
|
|
768
|
+
sessionWorkspace: args.sessionWorkspace,
|
|
769
|
+
intendedWorkspace: args.intendedWorkspace,
|
|
770
|
+
transcriptWorkspace,
|
|
771
|
+
fallbackReason,
|
|
772
|
+
nativeSource,
|
|
773
|
+
sourcePath,
|
|
774
|
+
sourceMtimeMs,
|
|
775
|
+
nativeHistoryCoverage: coverageHint,
|
|
776
|
+
partialReason,
|
|
777
|
+
unavailableReason,
|
|
778
|
+
nativeMessages,
|
|
779
|
+
ptyMessages: args.ptyMessages,
|
|
780
|
+
returnedMessages: decision.selected === 'native-history' ? nativeMessages : args.ptyMessages,
|
|
781
|
+
safeMapping: args.safeMapping,
|
|
782
|
+
// freshEnough is a v1 concept the machine does not model directly.
|
|
783
|
+
// We surface lockState.locked here so v1 consumers reading
|
|
784
|
+
// staleness.freshEnough still get a meaningful boolean.
|
|
785
|
+
freshEnough: decision.lockState.locked,
|
|
786
|
+
ptyStatusApprovalOnly,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
decision,
|
|
791
|
+
messageSource,
|
|
792
|
+
nativeMessages,
|
|
793
|
+
nativeSelected: decision.selected === 'native-history',
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function buildObservationForCli(
|
|
798
|
+
args: {
|
|
799
|
+
providerType: string;
|
|
800
|
+
sessionId: string;
|
|
801
|
+
nativeHistoryResult: any | null;
|
|
802
|
+
nativeHistoryError?: unknown;
|
|
803
|
+
safeMapping: boolean;
|
|
804
|
+
},
|
|
805
|
+
supportsNative: boolean,
|
|
806
|
+
): ChatSourceObservation {
|
|
807
|
+
if (!supportsNative) {
|
|
808
|
+
return { kind: 'native_unavailable', reason: 'provider_not_supported' };
|
|
809
|
+
}
|
|
810
|
+
if (args.nativeHistoryError) {
|
|
811
|
+
return { kind: 'native_unavailable', reason: 'read_error' };
|
|
812
|
+
}
|
|
813
|
+
const result = args.nativeHistoryResult;
|
|
814
|
+
if (!result || typeof result !== 'object') {
|
|
815
|
+
return { kind: 'native_unavailable', reason: 'read_error' };
|
|
816
|
+
}
|
|
817
|
+
const source = typeof result.source === 'string' ? result.source : '';
|
|
818
|
+
if (source && source !== 'provider-native') {
|
|
819
|
+
// 'native-unavailable' or other producer-side declined source.
|
|
820
|
+
return { kind: 'native_unavailable', reason: source === 'native-unavailable' ? 'empty' : 'not_native_source' };
|
|
821
|
+
}
|
|
822
|
+
const messages = Array.isArray(result.messages) ? result.messages : [];
|
|
823
|
+
if (messages.length === 0) {
|
|
824
|
+
return { kind: 'native_unavailable', reason: 'empty' };
|
|
825
|
+
}
|
|
826
|
+
const coverage = typeof result.nativeHistoryCoverage === 'string'
|
|
827
|
+
? result.nativeHistoryCoverage
|
|
828
|
+
: 'tail';
|
|
829
|
+
if (coverage === 'unavailable') {
|
|
830
|
+
return { kind: 'native_unavailable', reason: 'coverage_unavailable' };
|
|
831
|
+
}
|
|
832
|
+
return buildV1NativePresentObservation({
|
|
833
|
+
providerType: args.providerType,
|
|
834
|
+
sessionId: args.sessionId,
|
|
835
|
+
messages,
|
|
836
|
+
coverage: coverage === 'full' || coverage === 'tail' || coverage === 'current-turn' || coverage === 'partial'
|
|
837
|
+
? coverage
|
|
838
|
+
: 'tail',
|
|
839
|
+
safeMapping: args.safeMapping,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function extractNativeMessagesFromResult(providerType: string, result: any): ChatMessage[] {
|
|
844
|
+
if (!result || !Array.isArray(result.messages)) return [];
|
|
845
|
+
return normalizeNativeHistoryMessages(
|
|
846
|
+
providerType,
|
|
847
|
+
result.messages as ChatMessage[],
|
|
848
|
+
typeof result.providerSessionId === 'string' ? result.providerSessionId : undefined,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* ptyStatusApprovalOnly is true when the daemon should treat PTY content as
|
|
854
|
+
* status/approval signal only (not as chat messages). v1 set this to `true`
|
|
855
|
+
* whenever native-history was selected as the source, and `false` otherwise.
|
|
856
|
+
* The machine equivalent: when native is the source we want PTY suppressed.
|
|
857
|
+
*/
|
|
858
|
+
function primaryPtyApprovalOnlyFor(_cliType: string, nativeSelected: boolean): boolean {
|
|
859
|
+
return nativeSelected;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Codex-only unsafe-native fallback: when the primary native fetch produced
|
|
864
|
+
* unsafe-mapping data, v1 attempted to recover by reading exact runtime
|
|
865
|
+
* mirror messages, runtime input ACK messages, or by trusting the current-
|
|
866
|
+
* runtime PTY when safely attributed. None of this is the machine's
|
|
867
|
+
* responsibility — the machine already decided pty-parser. This helper
|
|
868
|
+
* preserves the daemon-side message selection and annotates messageSource.
|
|
869
|
+
*/
|
|
870
|
+
function applyUnsafeNativeDaemonFallback(args: {
|
|
871
|
+
providerType: string;
|
|
872
|
+
adapter: CliAdapter;
|
|
873
|
+
helpers: CommandHelpers;
|
|
874
|
+
readChatArgs: any;
|
|
875
|
+
sessionWorkspace?: string;
|
|
876
|
+
intendedWorkspace?: string;
|
|
877
|
+
ptyMessages: ChatMessage[];
|
|
878
|
+
nativeHistoryLimit: number;
|
|
879
|
+
provider?: ProviderModule;
|
|
880
|
+
messageSourceRef: { set(value: Record<string, unknown>): void; get(): Record<string, unknown> };
|
|
881
|
+
apply(selection: {
|
|
882
|
+
messages: ChatMessage[];
|
|
883
|
+
transcriptAuthority?: 'provider' | 'daemon';
|
|
884
|
+
coverage?: 'full' | 'tail' | 'current-turn';
|
|
885
|
+
status?: string;
|
|
886
|
+
}): void;
|
|
887
|
+
activeModal: unknown;
|
|
888
|
+
returnedStatus: string;
|
|
889
|
+
coverage?: 'full' | 'tail' | 'current-turn';
|
|
890
|
+
}): void {
|
|
891
|
+
if (args.adapter.cliType !== 'codex-cli') {
|
|
892
|
+
// Only codex-cli had v1 daemon mirror recovery. Other providers skip.
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const ms = args.messageSourceRef.get();
|
|
896
|
+
const fallbackReason = typeof ms.fallbackReason === 'string' ? ms.fallbackReason : '';
|
|
897
|
+
if (!isUnsafeNativeTranscriptFallback(fallbackReason)) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const safeCurrentRuntimePtyMessages = isCurrentRuntimePtySafelyAttributed({
|
|
901
|
+
adapter: args.adapter,
|
|
902
|
+
helpers: args.helpers,
|
|
903
|
+
readChatArgs: args.readChatArgs,
|
|
904
|
+
sessionWorkspace: args.sessionWorkspace,
|
|
905
|
+
intendedWorkspace: args.intendedWorkspace,
|
|
906
|
+
ptyMessages: args.ptyMessages,
|
|
907
|
+
});
|
|
908
|
+
if (safeCurrentRuntimePtyMessages) {
|
|
909
|
+
args.apply({
|
|
910
|
+
messages: args.ptyMessages,
|
|
911
|
+
transcriptAuthority: 'daemon',
|
|
912
|
+
coverage: args.coverage || 'current-turn',
|
|
913
|
+
status: args.returnedStatus,
|
|
914
|
+
});
|
|
915
|
+
const next = { ...ms, selectedDaemonSource: 'current-runtime-pty', transcriptAuthority: 'daemon', runtimeMappingSafe: true };
|
|
916
|
+
args.messageSourceRef.set(next);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const safeRuntimeAckMessages = selectRuntimeInputAckMessages(args.ptyMessages);
|
|
920
|
+
if (safeRuntimeAckMessages.length > 0) {
|
|
921
|
+
args.apply({
|
|
922
|
+
messages: safeRuntimeAckMessages,
|
|
923
|
+
transcriptAuthority: 'daemon',
|
|
924
|
+
coverage: 'tail',
|
|
925
|
+
status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
|
|
926
|
+
});
|
|
927
|
+
const next = { ...ms, ptyStatusApprovalOnly: true };
|
|
928
|
+
args.messageSourceRef.set(next);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const exactRuntimeMirrorMessages = readExactRuntimeMirrorMessages({
|
|
932
|
+
providerType: args.providerType,
|
|
933
|
+
targetSessionId: typeof args.readChatArgs?.targetSessionId === 'string' ? args.readChatArgs.targetSessionId : undefined,
|
|
934
|
+
currentSessionId: typeof (args.helpers.currentSession as any)?.sessionId === 'string' ? (args.helpers.currentSession as any).sessionId : undefined,
|
|
935
|
+
tailLimit: args.nativeHistoryLimit,
|
|
936
|
+
historyBehavior: args.provider?.historyBehavior,
|
|
937
|
+
});
|
|
938
|
+
if (exactRuntimeMirrorMessages.length > 0) {
|
|
939
|
+
args.apply({
|
|
940
|
+
messages: exactRuntimeMirrorMessages,
|
|
941
|
+
transcriptAuthority: 'daemon',
|
|
942
|
+
coverage: 'tail',
|
|
943
|
+
status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
|
|
944
|
+
});
|
|
945
|
+
const next = { ...ms, selectedDaemonSource: 'exact-runtime-mirror', transcriptAuthority: 'daemon', ptyStatusApprovalOnly: true };
|
|
946
|
+
args.messageSourceRef.set(next);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
// No daemon mirror available — keep PTY messages as-is (still pty-parser
|
|
950
|
+
// selection); just coerce status for waiting_approval consistency.
|
|
951
|
+
args.apply({
|
|
952
|
+
messages: args.ptyMessages,
|
|
953
|
+
coverage: args.coverage,
|
|
954
|
+
status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
|
|
955
|
+
});
|
|
956
|
+
const next = { ...ms, ptyStatusApprovalOnly: true };
|
|
957
|
+
args.messageSourceRef.set(next);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// (A2.2) buildNativeHistoryFallbackReason removed. ChatSourceMachine emits a
|
|
961
|
+
// ChatSourceTransitionCause; causeToLegacyFallbackReason maps it back to the
|
|
962
|
+
// v1 vocabulary for response compatibility. A3 deletes the v1 vocabulary
|
|
963
|
+
// entirely and surfaces stateTransition/lockState directly.
|
|
964
|
+
|
|
965
|
+
function isUnsafeNativeTranscriptFallback(reason?: string): boolean {
|
|
966
|
+
const value = String(reason || '').trim();
|
|
967
|
+
return value.startsWith('native_history_unavailable')
|
|
968
|
+
|| value === 'native_history_not_safely_mapped'
|
|
969
|
+
|| value === 'native_history_stale'
|
|
970
|
+
|| value === 'native_history_partial';
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function coerceUnsafeNativeFallbackStatus(status: string, activeModal: unknown): string {
|
|
974
|
+
if (status === 'waiting_approval' && activeModal) return status;
|
|
975
|
+
return 'idle';
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function isRuntimeInputAckMessage(message: ChatMessage | undefined): boolean {
|
|
979
|
+
if (!message || typeof message !== 'object') return false;
|
|
980
|
+
const role = String((message as any).role || '').trim().toLowerCase();
|
|
981
|
+
if (role !== 'user' && role !== 'human') return false;
|
|
982
|
+
const meta = (message as any).meta;
|
|
983
|
+
return !!meta && typeof meta === 'object' && !Array.isArray(meta) && meta.runtimeInputAck === true;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function selectRuntimeInputAckMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
987
|
+
return messages.filter((message) => isRuntimeInputAckMessage(message));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function readExactRuntimeMirrorMessages(args: {
|
|
991
|
+
providerType: string;
|
|
992
|
+
targetSessionId?: string;
|
|
993
|
+
currentSessionId?: string;
|
|
994
|
+
tailLimit: number;
|
|
995
|
+
historyBehavior?: ProviderModule['historyBehavior'];
|
|
996
|
+
}): ChatMessage[] {
|
|
997
|
+
const targetSessionId = String(args.targetSessionId || '').trim();
|
|
998
|
+
const currentSessionId = String(args.currentSessionId || '').trim();
|
|
999
|
+
if (!targetSessionId || targetSessionId !== currentSessionId) return [];
|
|
1000
|
+
|
|
1001
|
+
const history = readChatHistory(
|
|
1002
|
+
args.providerType,
|
|
1003
|
+
0,
|
|
1004
|
+
Math.max(args.tailLimit || 0, HOT_TAIL_MIN_LIMIT),
|
|
1005
|
+
targetSessionId,
|
|
1006
|
+
0,
|
|
1007
|
+
args.historyBehavior,
|
|
1008
|
+
);
|
|
1009
|
+
return normalizeChatMessages((history.messages || []) as ChatMessage[])
|
|
1010
|
+
.filter((message) => {
|
|
1011
|
+
const historySessionId = String((message as any).historySessionId || '').trim();
|
|
1012
|
+
const instanceId = String((message as any).instanceId || '').trim();
|
|
1013
|
+
return historySessionId === targetSessionId || instanceId === targetSessionId;
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
function normalizeComparableWorkspace(value: unknown): string {
|
|
1017
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
1018
|
+
if (!text) return '';
|
|
1019
|
+
return path.resolve(text);
|
|
1020
|
+
}
|
|
1021
|
+
function isCurrentRuntimePtySafelyAttributed(args: {
|
|
1022
|
+
adapter: CliAdapter;
|
|
1023
|
+
helpers: CommandHelpers;
|
|
1024
|
+
readChatArgs: any;
|
|
1025
|
+
sessionWorkspace?: string;
|
|
1026
|
+
intendedWorkspace?: string;
|
|
1027
|
+
ptyMessages: ChatMessage[];
|
|
1028
|
+
}): boolean {
|
|
1029
|
+
if (args.adapter.cliType !== 'codex-cli') return false;
|
|
1030
|
+
if (!Array.isArray(args.ptyMessages) || args.ptyMessages.length === 0) return false;
|
|
1031
|
+
const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
|
|
1032
|
+
? args.readChatArgs.targetSessionId.trim()
|
|
1033
|
+
: '';
|
|
1034
|
+
const currentSession = args.helpers.currentSession as any;
|
|
1035
|
+
const currentSessionId = typeof currentSession?.sessionId === 'string'
|
|
1036
|
+
? currentSession.sessionId.trim()
|
|
1037
|
+
: '';
|
|
1038
|
+
if (!targetSessionId || !currentSessionId || targetSessionId !== currentSessionId) return false;
|
|
1039
|
+
|
|
1040
|
+
const runtimeMeta = typeof (args.adapter as any).getRuntimeMetadata === 'function'
|
|
1041
|
+
? (args.adapter as any).getRuntimeMetadata()
|
|
1042
|
+
: null;
|
|
1043
|
+
const runtimeId = typeof runtimeMeta?.runtimeId === 'string' ? runtimeMeta.runtimeId.trim() : '';
|
|
1044
|
+
if (!runtimeId || runtimeId !== targetSessionId) return false;
|
|
1045
|
+
const surfaceKind = typeof runtimeMeta?.surfaceKind === 'string' ? runtimeMeta.surfaceKind : '';
|
|
1046
|
+
if (surfaceKind === 'inactive_record' || surfaceKind === 'recovery_snapshot') return false;
|
|
1047
|
+
|
|
1048
|
+
const sessionWorkspace = normalizeComparableWorkspace(args.sessionWorkspace);
|
|
1049
|
+
const adapterWorkspace = normalizeComparableWorkspace(args.adapter.workingDir);
|
|
1050
|
+
if (!sessionWorkspace || !adapterWorkspace || sessionWorkspace !== adapterWorkspace) return false;
|
|
1051
|
+
const intendedWorkspace = normalizeComparableWorkspace(args.intendedWorkspace);
|
|
1052
|
+
if (intendedWorkspace && intendedWorkspace !== sessionWorkspace) return false;
|
|
1053
|
+
|
|
1054
|
+
const registryEntry = args.helpers.ctx?.sessionRegistry?.get?.(targetSessionId) as any;
|
|
1055
|
+
const registryInstanceKey = typeof registryEntry?.adapterKey === 'string' && registryEntry.adapterKey.trim()
|
|
1056
|
+
? registryEntry.adapterKey.trim()
|
|
1057
|
+
: typeof registryEntry?.instanceKey === 'string' && registryEntry.instanceKey.trim()
|
|
1058
|
+
? registryEntry.instanceKey.trim()
|
|
1059
|
+
: '';
|
|
1060
|
+
if (registryInstanceKey) {
|
|
1061
|
+
const targetInstance = args.helpers.ctx?.instanceManager?.getInstance?.(registryInstanceKey);
|
|
1062
|
+
if (targetInstance) {
|
|
1063
|
+
const instanceType = typeof (targetInstance as any).type === 'string' ? (targetInstance as any).type : '';
|
|
1064
|
+
if (instanceType && instanceType !== args.adapter.cliType) return false;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function supportsCliNativeTranscript(providerType: string, provider?: ProviderModule): boolean {
|
|
1072
|
+
// Preferred path: the provider module declares canonicalHistory in its
|
|
1073
|
+
// provider.json. We trust that declaration regardless of the legacy
|
|
1074
|
+
// allow-list. A2 will additionally require canonicalHistory.contractVersion
|
|
1075
|
+
// to be a supported value (transcript-v2.ts).
|
|
1076
|
+
if (provider?.category === 'cli' && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
// Last-resort fallback for early call sites where the provider module is
|
|
1080
|
+
// not yet loaded. Warn once per provider type so this dependency is visible
|
|
1081
|
+
// and can be removed in A2.
|
|
1082
|
+
if (CLI_NATIVE_TRANSCRIPT_PROVIDERS.has(providerType)) {
|
|
1083
|
+
warnLegacyNativeAllowlistHit(providerType);
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function getComparableVisibleText(message: ChatMessage | undefined): string {
|
|
1090
|
+
if (!message) return '';
|
|
1091
|
+
const role = String((message as any).role || '').trim().toLowerCase();
|
|
1092
|
+
if (role !== 'user' && role !== 'assistant') return '';
|
|
1093
|
+
const kind = String((message as any).kind || 'standard').trim().toLowerCase();
|
|
1094
|
+
if (kind && kind !== 'standard') return '';
|
|
1095
|
+
const content = flattenContent((message as any).content).replace(/\s+/g, ' ').trim();
|
|
1096
|
+
return content;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function hasOverlappingVisibleConversationText(nativeMessages: ChatMessage[], ptyMessages: ChatMessage[]): boolean {
|
|
1100
|
+
const nativeTexts = nativeMessages.map(getComparableVisibleText).filter(Boolean);
|
|
1101
|
+
const ptyTexts = ptyMessages.map(getComparableVisibleText).filter(Boolean);
|
|
1102
|
+
if (nativeTexts.length === 0 || ptyTexts.length === 0) return false;
|
|
1103
|
+
for (const nativeText of nativeTexts) {
|
|
1104
|
+
for (const ptyText of ptyTexts) {
|
|
1105
|
+
if (nativeText === ptyText) return true;
|
|
1106
|
+
const shorter = nativeText.length <= ptyText.length ? nativeText : ptyText;
|
|
1107
|
+
const longer = nativeText.length <= ptyText.length ? ptyText : nativeText;
|
|
1108
|
+
if (shorter.length >= 32 && longer.includes(shorter)) return true;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function hasSafeNativeHistoryMapping(args: {
|
|
1115
|
+
historySessionId?: string;
|
|
1116
|
+
providerSessionId?: string;
|
|
1117
|
+
workspace?: string;
|
|
1118
|
+
nativeMessages: ChatMessage[];
|
|
1119
|
+
ptyMessages?: ChatMessage[];
|
|
1120
|
+
requireWorkspaceContentOverlap?: boolean;
|
|
1121
|
+
}): boolean {
|
|
1122
|
+
const isCoordinatorTranscript = args.nativeMessages.some((m: any) => {
|
|
1123
|
+
const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
|
|
1124
|
+
return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat') || text.includes('mesh_launch_session');
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
|
|
1128
|
+
if (explicitSessionId) {
|
|
1129
|
+
const expectedWorkspace = normalizeComparableWorkspace(args.workspace);
|
|
1130
|
+
const declaredWorkspaces = args.nativeMessages
|
|
1131
|
+
.map((message: any) => normalizeComparableWorkspace(message?.workspace))
|
|
1132
|
+
.filter(Boolean);
|
|
1133
|
+
if (
|
|
1134
|
+
expectedWorkspace
|
|
1135
|
+
&& declaredWorkspaces.length > 0
|
|
1136
|
+
&& !declaredWorkspaces.some((workspace) => workspace === expectedWorkspace)
|
|
1137
|
+
) {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
const messageSessionIds = args.nativeMessages
|
|
1141
|
+
.map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
|
|
1142
|
+
.filter(Boolean);
|
|
1143
|
+
if (messageSessionIds.length > 0) {
|
|
1144
|
+
return messageSessionIds.some((id) => id === explicitSessionId);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Messages carry no historySessionId — cannot confirm they belong to the requested session.
|
|
1148
|
+
// Only allow a coordinator transcript that is also confirmed by the PTY side; otherwise
|
|
1149
|
+
// fail closed so a same-workspace session's history is never silently accepted.
|
|
1150
|
+
if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
|
|
1151
|
+
const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
|
|
1152
|
+
const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
|
|
1153
|
+
return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
|
|
1154
|
+
});
|
|
1155
|
+
return ptyHasCoordinator;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// No historySessionId in messages and no coordinator cross-check: fail closed.
|
|
1159
|
+
// Workspace-only matching must not override an explicit session identity.
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
const workspace = String(args.workspace || '').trim();
|
|
1163
|
+
if (!workspace) return false;
|
|
1164
|
+
const workspaceMatches = args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
|
|
1165
|
+
if (!workspaceMatches) return false;
|
|
1166
|
+
|
|
1167
|
+
if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
|
|
1168
|
+
const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
|
|
1169
|
+
const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
|
|
1170
|
+
return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
|
|
1171
|
+
});
|
|
1172
|
+
if (!ptyHasCoordinator) {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (!args.requireWorkspaceContentOverlap) return true;
|
|
1178
|
+
return hasOverlappingVisibleConversationText(args.nativeMessages, args.ptyMessages || []);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Provenance boundary: workspace-only native history lookup is never safe
|
|
1182
|
+
// because multiple concurrent sessions sharing the same cwd would alias each
|
|
1183
|
+
// other. historySessionId (the provider-native session key) is required to
|
|
1184
|
+
// establish ownership. hasSafeNativeHistoryMapping() enforces the same
|
|
1185
|
+
// invariant after the read; both guards must hold for native history to be used.
|
|
1186
|
+
/**
|
|
1187
|
+
* The session id a native-history read should be scoped to: the explicit
|
|
1188
|
+
* targetSessionId when the caller named one (reading a specific/worker
|
|
1189
|
+
* session), otherwise the current live session (a self / dashboard read where
|
|
1190
|
+
* the current session IS the one being read). getTargetedCliAdapter already
|
|
1191
|
+
* uses this same fallback to resolve the adapter; the native-history floor and
|
|
1192
|
+
* claim-owner token must use it too. Without the fallback, a self-read arrives
|
|
1193
|
+
* with no targetSessionId → the floor collapses to undefined (→0) and the
|
|
1194
|
+
* antigravity claim-owner token collapses to '' → pickUnboundConversationDb
|
|
1195
|
+
* drops out of its spawn-floor branch into newest-by-mtime and binds whichever
|
|
1196
|
+
* conversation .db was written most recently. For an antigravity MAGI
|
|
1197
|
+
* coordinator that is exactly a co-located replica's .db (the replica finished
|
|
1198
|
+
* its turn last), so the coordinator's read cross-wires onto the replica's
|
|
1199
|
+
* conversation instead of its own (ANTIGRAVITY coordinator↔replica crosswire).
|
|
1200
|
+
*/
|
|
1201
|
+
function effectiveReadSessionId(h: CommandHelpers, targetSessionId: string | undefined): string {
|
|
1202
|
+
const explicit = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
|
|
1203
|
+
if (explicit) return explicit;
|
|
1204
|
+
const current = (h.currentSession as any)?.sessionId;
|
|
1205
|
+
return typeof current === 'string' ? current.trim() : '';
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Pull the session's spawnedAtMs out of the registry. Native-history
|
|
1210
|
+
* file pickers use it as a "files older than this can't be from this
|
|
1211
|
+
* session" floor; without it a fresh dashboard view would inherit the
|
|
1212
|
+
* previous session's transcript whenever its file happened to be the
|
|
1213
|
+
* newest match. Returns undefined when the session isn't registered
|
|
1214
|
+
* (e.g. read_chat before the live session was wired up) — the executor
|
|
1215
|
+
* treats undefined as "no floor". Resolves the effective session id
|
|
1216
|
+
* (targetSessionId or the current live session) so a self-read still gets its
|
|
1217
|
+
* real spawn floor rather than 0.
|
|
1218
|
+
*/
|
|
1219
|
+
function sessionStartedAtMsFromRegistry(h: CommandHelpers, targetSessionId: string | undefined): number | undefined {
|
|
1220
|
+
const sid = effectiveReadSessionId(h, targetSessionId);
|
|
1221
|
+
if (!sid) return undefined;
|
|
1222
|
+
const target = h.ctx?.sessionRegistry?.get?.(sid);
|
|
1223
|
+
return typeof target?.spawnedAtMs === 'number' ? target.spawnedAtMs : undefined;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Pull the env vars the daemon set when it spawned this session's CLI.
|
|
1228
|
+
* Mesh coordinator points hermes at a per-coordinator HERMES_HOME so
|
|
1229
|
+
* the native-history reader needs that override to find the right
|
|
1230
|
+
* state.db; without it the reader sees ~/.hermes/state.db and misses
|
|
1231
|
+
* every coordinator-session transcript.
|
|
1232
|
+
*
|
|
1233
|
+
* Returns undefined when no SpecCliAdapter is in play (legacy
|
|
1234
|
+
* providers / CDP) or when the adapter exposes no spawn env.
|
|
1235
|
+
*/
|
|
1236
|
+
function sessionSpawnEnvFromAdapter(h: CommandHelpers, targetSessionId: string | undefined): Record<string, string> | undefined {
|
|
1237
|
+
const adapter = getTargetedCliAdapter(h, { targetSessionId }, undefined);
|
|
1238
|
+
if (!adapter || typeof adapter.getRuntimeMetadata !== 'function') return undefined;
|
|
1239
|
+
const meta = adapter.getRuntimeMetadata() as Record<string, unknown> | undefined;
|
|
1240
|
+
const env = meta && typeof meta === 'object' ? (meta as Record<string, unknown>).spawnedEnv : undefined;
|
|
1241
|
+
return env && typeof env === 'object' ? env as Record<string, string> : undefined;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
function readCliProviderNativeHistory(agentStr: string, args: {
|
|
1246
|
+
canonicalHistory?: ProviderModule['canonicalHistory'];
|
|
1247
|
+
historySessionId?: string;
|
|
1248
|
+
workspace?: string;
|
|
1249
|
+
offset: number;
|
|
1250
|
+
limit: number;
|
|
1251
|
+
excludeRecentCount: number;
|
|
1252
|
+
historyBehavior?: ProviderModule['historyBehavior'];
|
|
1253
|
+
scripts?: ProviderScripts;
|
|
1254
|
+
excludeInProgressTurn?: boolean;
|
|
1255
|
+
sessionStartedAtMs?: number;
|
|
1256
|
+
envOverrides?: Record<string, string>;
|
|
1257
|
+
// Last provider-native session id previously bound for this mesh session
|
|
1258
|
+
// (see lastBoundProviderSessionIdByMeshSession). When historySessionId is
|
|
1259
|
+
// empty and no live session can be bound (post-turn read), reuse this pin so
|
|
1260
|
+
// the native query runs against the known session instead of fail-closing.
|
|
1261
|
+
pinnedProviderSessionId?: string;
|
|
1262
|
+
// Opt-in last-resort: when there is no caller session id, no live binding,
|
|
1263
|
+
// and NO pin was ever recorded, allow a workspace-scoped read (newest
|
|
1264
|
+
// session in state.db with rows for this workspace). Strictly behind the pin
|
|
1265
|
+
// — it never fires when pinnedProviderSessionId is set — and still subject to
|
|
1266
|
+
// the downstream hasSafeNativeHistoryMapping workspace-overlap gate. Only the
|
|
1267
|
+
// read_chat path opts in, and only with a concrete workspace.
|
|
1268
|
+
allowWorkspaceLatestFallback?: boolean;
|
|
1269
|
+
// ADHDev session id of the reading session (== the session registry's
|
|
1270
|
+
// sessionId == the provider instance's instanceId). Threaded to the
|
|
1271
|
+
// native-history dispatcher so antigravity's conversation-claim owner token
|
|
1272
|
+
// is keyed on this stable identity and matches the instance-side token —
|
|
1273
|
+
// without it two concurrent antigravity sessions cross-bind each other's
|
|
1274
|
+
// conversation .db.
|
|
1275
|
+
instanceId?: string;
|
|
1276
|
+
}): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
|
|
1277
|
+
const canBindFromLiveSession = !args.historySessionId
|
|
1278
|
+
&& typeof args.sessionStartedAtMs === 'number'
|
|
1279
|
+
&& args.sessionStartedAtMs > 0
|
|
1280
|
+
&& typeof args.workspace === 'string'
|
|
1281
|
+
&& args.workspace.trim().length > 0;
|
|
1282
|
+
const pinnedProviderSessionId = typeof args.pinnedProviderSessionId === 'string'
|
|
1283
|
+
? args.pinnedProviderSessionId.trim()
|
|
1284
|
+
: '';
|
|
1285
|
+
// Pin reuse (PRIMARY): a later read whose live binding is gone
|
|
1286
|
+
// (historySessionId empty, no live spawnedAtMs) can still resolve to the
|
|
1287
|
+
// session it was last bound to. Read THAT session directly by threading the
|
|
1288
|
+
// pin through as historySessionId — same code path an explicit session read
|
|
1289
|
+
// takes — instead of fail-closing. Never overrides a caller-supplied
|
|
1290
|
+
// historySessionId; only kicks in when there is none.
|
|
1291
|
+
const effectiveHistorySessionId = args.historySessionId || (!canBindFromLiveSession ? pinnedProviderSessionId : '');
|
|
1292
|
+
// Last-resort workspace-latest (b): only when nothing above resolved a
|
|
1293
|
+
// session id AND no pin exists AND the caller opted in with a workspace.
|
|
1294
|
+
// Strictly behind pin reuse — pinnedProviderSessionId being set disables it.
|
|
1295
|
+
const workspaceLatestFallback = !effectiveHistorySessionId
|
|
1296
|
+
&& !canBindFromLiveSession
|
|
1297
|
+
&& !pinnedProviderSessionId
|
|
1298
|
+
&& args.allowWorkspaceLatestFallback === true
|
|
1299
|
+
&& typeof args.workspace === 'string'
|
|
1300
|
+
&& args.workspace.trim().length > 0;
|
|
1301
|
+
if (!effectiveHistorySessionId && !canBindFromLiveSession && !workspaceLatestFallback) {
|
|
1302
|
+
// No caller session id, no live binding, no known pin, no opted-in
|
|
1303
|
+
// workspace-latest. This is the genuinely-unresolvable case — a
|
|
1304
|
+
// workspace-only lookup here could alias a concurrent session sharing the
|
|
1305
|
+
// cwd, so fail closed as before. The pin/live/workspace-latest paths are
|
|
1306
|
+
// all checked AHEAD of this so a resolvable session is never dropped here.
|
|
1307
|
+
return {
|
|
1308
|
+
messages: [],
|
|
1309
|
+
hasMore: false,
|
|
1310
|
+
source: 'native-unavailable',
|
|
1311
|
+
unavailableReason: 'native_history_workspace_only_lookup_unsafe',
|
|
1312
|
+
lookup: 'session',
|
|
1313
|
+
} as ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' };
|
|
1314
|
+
}
|
|
1315
|
+
const sessionHistory = readProviderChatHistory(agentStr, {
|
|
1316
|
+
canonicalHistory: args.canonicalHistory,
|
|
1317
|
+
historySessionId: effectiveHistorySessionId || undefined,
|
|
1318
|
+
workspace: args.workspace,
|
|
1319
|
+
offset: args.offset,
|
|
1320
|
+
limit: args.limit,
|
|
1321
|
+
excludeRecentCount: args.excludeRecentCount,
|
|
1322
|
+
historyBehavior: args.historyBehavior,
|
|
1323
|
+
scripts: args.scripts as any,
|
|
1324
|
+
excludeInProgressTurn: args.excludeInProgressTurn,
|
|
1325
|
+
sessionStartedAtMs: args.sessionStartedAtMs,
|
|
1326
|
+
envOverrides: args.envOverrides,
|
|
1327
|
+
instanceId: args.instanceId,
|
|
1328
|
+
});
|
|
1329
|
+
const boundProviderSessionId = typeof (sessionHistory as any)?.providerSessionId === 'string'
|
|
1330
|
+
? (sessionHistory as any).providerSessionId.trim()
|
|
1331
|
+
: '';
|
|
1332
|
+
// A fresh live session can be bound without a provider id when the native
|
|
1333
|
+
// reader matched both cwd and session_meta.timestamp to spawnedAtMs. A
|
|
1334
|
+
// pin-bound read is always session-scoped (we passed an explicit id).
|
|
1335
|
+
return {
|
|
1336
|
+
...(sessionHistory as any),
|
|
1337
|
+
lookup: effectiveHistorySessionId || (canBindFromLiveSession && boundProviderSessionId)
|
|
1338
|
+
? 'session'
|
|
1339
|
+
: 'workspace',
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function readLiveCodexWorkspaceNativeHistory(agentStr: string, args: {
|
|
1344
|
+
canonicalHistory?: ProviderModule['canonicalHistory'];
|
|
1345
|
+
workspace?: string;
|
|
1346
|
+
offset: number;
|
|
1347
|
+
limit: number;
|
|
1348
|
+
excludeRecentCount: number;
|
|
1349
|
+
historyBehavior?: ProviderModule['historyBehavior'];
|
|
1350
|
+
scripts?: ProviderScripts;
|
|
1351
|
+
}): (ReturnType<typeof readProviderChatHistory> & { lookup: 'workspace' }) | null {
|
|
1352
|
+
if (agentStr !== 'codex-cli') return null;
|
|
1353
|
+
const workspace = typeof args.workspace === 'string' ? args.workspace.trim() : '';
|
|
1354
|
+
if (!workspace) return null;
|
|
1355
|
+
const history = readProviderChatHistory(agentStr, {
|
|
1356
|
+
canonicalHistory: args.canonicalHistory,
|
|
1357
|
+
workspace,
|
|
1358
|
+
offset: args.offset,
|
|
1359
|
+
limit: args.limit,
|
|
1360
|
+
excludeRecentCount: args.excludeRecentCount,
|
|
1361
|
+
historyBehavior: args.historyBehavior,
|
|
1362
|
+
scripts: args.scripts as any,
|
|
1363
|
+
});
|
|
1364
|
+
return { ...(history as any), lookup: 'workspace' };
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// (A2.2) isNativeHistoryFreshEnough removed. The v1 freshness comparison
|
|
1368
|
+
// (native_newest vs pty_newest with a 5-minute mtime grace window) was the
|
|
1369
|
+
// direct cause of the plipping behaviour: PTY arrived every turn so native
|
|
1370
|
+
// looked stale by default. ChatSourceMachine never compares native vs PTY
|
|
1371
|
+
// freshness — the lock holds across arbitrary PTY arrival. See
|
|
1372
|
+
// chat/source-machine.ts for the new semantics.
|
|
1373
|
+
|
|
1374
|
+
function shouldPreserveReadChatPayloadField(key: string): boolean {
|
|
1375
|
+
return key === 'messageSource' || key === 'transcriptProvenance';
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function updateMessageSourceReturnedCount(value: unknown, returnedMessageCount: number): unknown {
|
|
1379
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
|
|
1380
|
+
const record = value as Record<string, unknown>;
|
|
1381
|
+
const coverage = record.coverage && typeof record.coverage === 'object' && !Array.isArray(record.coverage)
|
|
1382
|
+
? record.coverage as Record<string, unknown>
|
|
1383
|
+
: undefined;
|
|
1384
|
+
if (!coverage) return value;
|
|
1385
|
+
return {
|
|
1386
|
+
...record,
|
|
1387
|
+
coverage: {
|
|
1388
|
+
...coverage,
|
|
1389
|
+
returnedMessageCount,
|
|
1390
|
+
},
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
|
|
1395
|
+
const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
|
|
1396
|
+
if (unitKey) return `read_chat:${unitKey}`;
|
|
1397
|
+
|
|
1398
|
+
const turnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
|
|
1399
|
+
if (!turnKey) return undefined;
|
|
1400
|
+
|
|
1401
|
+
let content = '';
|
|
1402
|
+
try {
|
|
1403
|
+
content = JSON.stringify(message.content ?? '');
|
|
1404
|
+
} catch {
|
|
1405
|
+
content = String(message.content ?? '');
|
|
1406
|
+
}
|
|
1407
|
+
return `read_chat:${turnKey}:${String(message.role || '').toLowerCase()}:${content}`;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function toHistoryPersistedMessages(messages: ChatMessage[]): Array<{
|
|
1411
|
+
role: string;
|
|
1412
|
+
content: string;
|
|
1413
|
+
receivedAt?: number;
|
|
1414
|
+
kind?: string;
|
|
1415
|
+
senderName?: string;
|
|
1416
|
+
historyDedupKey?: string;
|
|
1417
|
+
}> {
|
|
1418
|
+
return messages.map((message) => ({
|
|
1419
|
+
role: message.role,
|
|
1420
|
+
content: flattenContent(message.content),
|
|
1421
|
+
receivedAt: typeof message.receivedAt === 'number' ? message.receivedAt : undefined,
|
|
1422
|
+
kind: typeof message.kind === 'string' ? message.kind : undefined,
|
|
1423
|
+
senderName: typeof message.senderName === 'string' ? message.senderName : undefined,
|
|
1424
|
+
historyDedupKey: deriveHistoryDedupKey(message as ChatMessage & { _unitKey?: string; _turnKey?: string }),
|
|
1425
|
+
}));
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function buildFullTail(messages: ChatMessage[], tailLimit: number): {
|
|
1429
|
+
messages: ChatMessage[];
|
|
1430
|
+
totalMessages: number;
|
|
1431
|
+
} {
|
|
1432
|
+
const totalMessages = messages.length;
|
|
1433
|
+
const tailMessages = tailLimit > 0 ? messages.slice(-tailLimit) : messages;
|
|
1434
|
+
return {
|
|
1435
|
+
messages: tailMessages,
|
|
1436
|
+
totalMessages,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function hasNonEmptyModalButtons(activeModal: unknown): boolean {
|
|
1441
|
+
if (!activeModal || typeof activeModal !== 'object') return false;
|
|
1442
|
+
const buttons = (activeModal as { buttons?: unknown }).buttons;
|
|
1443
|
+
return Array.isArray(buttons) && buttons.some((button) => typeof button === 'string' && button.trim().length > 0);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown): string {
|
|
1447
|
+
const raw = typeof status === 'string' ? status.trim() : '';
|
|
1448
|
+
if (!raw) {
|
|
1449
|
+
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'idle';
|
|
1450
|
+
}
|
|
1451
|
+
switch (raw) {
|
|
1452
|
+
case 'starting':
|
|
1453
|
+
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'starting';
|
|
1454
|
+
case 'stopped':
|
|
1455
|
+
case 'disconnected':
|
|
1456
|
+
case 'not_monitored':
|
|
1457
|
+
return 'error';
|
|
1458
|
+
case 'waiting_approval':
|
|
1459
|
+
// The contract validator requires activeModal+buttons whenever
|
|
1460
|
+
// status is waiting_approval. If a producer/coercer set this
|
|
1461
|
+
// status without staging the modal yet (a race we hit with
|
|
1462
|
+
// codex-cli during tool approval setup), downgrade to a
|
|
1463
|
+
// generating-like status so readChat still returns successfully.
|
|
1464
|
+
// The next poll will pick up the modal once the provider has
|
|
1465
|
+
// emitted it.
|
|
1466
|
+
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
|
|
1467
|
+
default:
|
|
1468
|
+
return raw;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function isGeneratingLikeStatus(status: unknown): boolean {
|
|
1473
|
+
return status === 'generating' || status === 'streaming' || status === 'no_progress' || status === 'long_generating' || status === 'starting';
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function hasVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
|
|
1477
|
+
if (!Array.isArray(messages)) return false;
|
|
1478
|
+
return messages.some((message: any) => {
|
|
1479
|
+
if (!message || message.role !== 'assistant') return false;
|
|
1480
|
+
const kind = typeof message.kind === 'string' ? message.kind : 'standard';
|
|
1481
|
+
if (kind !== 'standard') return false;
|
|
1482
|
+
return String(message.content || '').trim().length > 0;
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function hasFinalVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
|
|
1487
|
+
if (!Array.isArray(messages)) return false;
|
|
1488
|
+
const visible = filterUserFacingChatMessages(messages as ChatMessage[]);
|
|
1489
|
+
const last = visible[visible.length - 1] as ChatMessage | undefined;
|
|
1490
|
+
const role = typeof last?.role === 'string' ? last.role.trim().toLowerCase() : '';
|
|
1491
|
+
const content = last ? flattenContent(last.content).trim() : '';
|
|
1492
|
+
return (role === 'assistant' || role === 'model') && content.length > 0;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
|
|
1496
|
+
if (!isGeneratingLikeStatus(parsedStatus)) return false;
|
|
1497
|
+
if (hasNonEmptyModalButtons(activeModal)) return false;
|
|
1498
|
+
const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
1499
|
+
if (adapterRawStatus !== 'idle') return false;
|
|
1500
|
+
if (typeof adapter.isProcessing === 'function' && adapter.isProcessing()) return false;
|
|
1501
|
+
return true;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any, parsedMessages?: unknown[]): string {
|
|
1505
|
+
const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
1506
|
+
if (adapterRawStatus === 'starting'
|
|
1507
|
+
&& isGeneratingLikeStatus(parsedStatus)
|
|
1508
|
+
&& !hasNonEmptyModalButtons(activeModal)
|
|
1509
|
+
&& Array.isArray(parsedMessages)
|
|
1510
|
+
&& parsedMessages.length === 0
|
|
1511
|
+
&& Array.isArray(adapterStatus?.messages)
|
|
1512
|
+
&& adapterStatus.messages.length === 0
|
|
1513
|
+
&& !(typeof adapter.isProcessing === 'function' && adapter.isProcessing())) {
|
|
1514
|
+
return 'starting';
|
|
1515
|
+
}
|
|
1516
|
+
if (
|
|
1517
|
+
isGeneratingLikeStatus(adapterRawStatus)
|
|
1518
|
+
&& parsedStatus === 'idle'
|
|
1519
|
+
&& !hasNonEmptyModalButtons(activeModal)
|
|
1520
|
+
&& !hasVisibleAssistantMessage(parsedMessages)
|
|
1521
|
+
) {
|
|
1522
|
+
return adapterRawStatus;
|
|
1523
|
+
}
|
|
1524
|
+
if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
|
|
1525
|
+
return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: string): ChatMessage[] {
|
|
1529
|
+
if (status !== 'idle') return messages;
|
|
1530
|
+
return messages.map((message) => {
|
|
1531
|
+
const meta = message.meta && typeof message.meta === 'object'
|
|
1532
|
+
? message.meta as Record<string, unknown>
|
|
1533
|
+
: undefined;
|
|
1534
|
+
const hasStreamingMeta = meta?.streaming === true;
|
|
1535
|
+
if (message.bubbleState !== 'streaming' && !hasStreamingMeta) return message;
|
|
1536
|
+
return {
|
|
1537
|
+
...message,
|
|
1538
|
+
...(message.bubbleState === 'streaming' ? { bubbleState: 'final' as const } : {}),
|
|
1539
|
+
...(hasStreamingMeta ? { meta: { ...meta, streaming: false } } : {}),
|
|
1540
|
+
};
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Collapse adjacent PTY messages whose canonical (whitespace-stripped)
|
|
1546
|
+
* content is identical, OR whose turn key + role/kind match.
|
|
1547
|
+
*
|
|
1548
|
+
* The PTY parser of some providers (hermes-cli observed in the wild)
|
|
1549
|
+
* emits the same logical assistant turn twice when the terminal re-wraps
|
|
1550
|
+
* the text at a different column. The two emissions differ in newline
|
|
1551
|
+
* position — and sometimes in a single inserted space next to punctuation
|
|
1552
|
+
* (e.g. `(수정 2개), upstream` vs `(수정 2개 ), upstream`), so a simple
|
|
1553
|
+
* `\s+ -> ' '` normalize cannot collapse them.
|
|
1554
|
+
*
|
|
1555
|
+
* Strategy:
|
|
1556
|
+
* 1. If both messages carry the same _turnKey + role + kind, they are
|
|
1557
|
+
* the same logical turn by construction. Collapse.
|
|
1558
|
+
* 2. Otherwise compare with all whitespace stripped — wrap variants
|
|
1559
|
+
* collapse to identical strings.
|
|
1560
|
+
*
|
|
1561
|
+
* Native-history paths run through pageHistoryRecords and already
|
|
1562
|
+
* collapse on a normalized signature; this helper is the PTY equivalent
|
|
1563
|
+
* the readChat sync path was missing.
|
|
1564
|
+
*/
|
|
1565
|
+
function collapseAdjacentDuplicateChatMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
1566
|
+
if (!Array.isArray(messages) || messages.length <= 1) return messages;
|
|
1567
|
+
const result: ChatMessage[] = [];
|
|
1568
|
+
let prevRoleKind = '';
|
|
1569
|
+
let prevStripped = '';
|
|
1570
|
+
for (const message of messages) {
|
|
1571
|
+
const role = typeof message.role === 'string' ? message.role : '';
|
|
1572
|
+
const kind = typeof message.kind === 'string' ? message.kind : 'standard';
|
|
1573
|
+
const content = typeof message.content === 'string'
|
|
1574
|
+
? message.content
|
|
1575
|
+
: (Array.isArray(message.content) ? message.content.map((p: any) => typeof p?.text === 'string' ? p.text : '').join('') : '');
|
|
1576
|
+
const strippedContent = content.replace(/\s+/g, '');
|
|
1577
|
+
// Empty content or system messages are passed through untouched.
|
|
1578
|
+
if (!strippedContent || role === 'system') {
|
|
1579
|
+
result.push(message);
|
|
1580
|
+
prevRoleKind = '';
|
|
1581
|
+
prevStripped = '';
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
const roleKind = `${role}:${kind}`;
|
|
1585
|
+
const sameStripped = strippedContent === prevStripped && roleKind === prevRoleKind;
|
|
1586
|
+
if (result.length > 0 && sameStripped) {
|
|
1587
|
+
// Adjacent duplicate after stripping all whitespace. Keep the
|
|
1588
|
+
// *later* copy because PTY's last emission usually has the most
|
|
1589
|
+
// complete formatting.
|
|
1590
|
+
result[result.length - 1] = message;
|
|
1591
|
+
prevRoleKind = roleKind;
|
|
1592
|
+
prevStripped = strippedContent;
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
result.push(message);
|
|
1596
|
+
prevRoleKind = roleKind;
|
|
1597
|
+
prevStripped = strippedContent;
|
|
1598
|
+
}
|
|
1599
|
+
return result;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function buildReadChatCommandResult(payload: Record<string, any>, args: any, h?: CommandHelpers): CommandResult {
|
|
1603
|
+
let validatedPayload: Record<string, any>;
|
|
1604
|
+
const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
|
|
1605
|
+
? payload.debugReadChat
|
|
1606
|
+
: undefined;
|
|
1607
|
+
try {
|
|
1608
|
+
validatedPayload = validateReadChatResultPayload({
|
|
1609
|
+
...payload,
|
|
1610
|
+
status: normalizeReadChatCommandStatus(payload?.status, payload?.activeModal),
|
|
1611
|
+
}, 'read_chat command result') as Record<string, any>;
|
|
1612
|
+
} catch (error: any) {
|
|
1613
|
+
return { success: false, error: error?.message || String(error) };
|
|
1614
|
+
}
|
|
1615
|
+
const messages = normalizeReadChatMessages(validatedPayload);
|
|
1616
|
+
// Last-mile coordinator-prompt filter. Different read_chat code paths
|
|
1617
|
+
// produce the final messages array (native-history main path, codex
|
|
1618
|
+
// exact-runtime-mirror fallback, daemon-side pty-parser, etc), so
|
|
1619
|
+
// applying it here means we don't have to thread the filter through
|
|
1620
|
+
// every one. Driven by the provider setting `showCoordinatorSystemPrompt`
|
|
1621
|
+
// + the coordinator-registry entry for the target session.
|
|
1622
|
+
const sessionIdHint = typeof args?.targetSessionId === 'string' ? args.targetSessionId
|
|
1623
|
+
: typeof args?.sessionId === 'string' ? args.sessionId
|
|
1624
|
+
: '';
|
|
1625
|
+
const providerHint = typeof args?.cliType === 'string' ? args.cliType
|
|
1626
|
+
: typeof args?.providerType === 'string' ? args.providerType
|
|
1627
|
+
: typeof args?.agentType === 'string' ? args.agentType
|
|
1628
|
+
: '';
|
|
1629
|
+
const filteredMessages = h
|
|
1630
|
+
? maybeHideCoordinatorPromptMessage(h, providerHint, sessionIdHint, messages)
|
|
1631
|
+
: messages;
|
|
1632
|
+
// By default read_chat returns only user-facing prose turns. When the
|
|
1633
|
+
// caller opts in with `includeActivity`, tool/terminal/thought activity
|
|
1634
|
+
// bubbles (e.g. the native transcript's tool calls and results) are kept
|
|
1635
|
+
// inline too, in chronological order, so a restored conversation can show
|
|
1636
|
+
// what the agent actually did — not just the prose around it.
|
|
1637
|
+
const includeActivity = args?.includeActivity === true || args?.includeActivity === 'true';
|
|
1638
|
+
const visibleMessages = includeActivity
|
|
1639
|
+
? filteredMessages.filter((m) => isUserFacingChatMessage(m) || isActivityChatMessage(m))
|
|
1640
|
+
: filterUserFacingChatMessages(filteredMessages);
|
|
1641
|
+
const sync = buildFullTail(visibleMessages, normalizeReadChatTailLimit(args));
|
|
1642
|
+
const hiddenMsgCount = Math.max(0, messages.length - visibleMessages.length);
|
|
1643
|
+
const preservedPayloadFields = Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key)));
|
|
1644
|
+
if (preservedPayloadFields.messageSource) {
|
|
1645
|
+
preservedPayloadFields.messageSource = updateMessageSourceReturnedCount(preservedPayloadFields.messageSource, sync.messages.length);
|
|
1646
|
+
}
|
|
1647
|
+
if (preservedPayloadFields.transcriptProvenance) {
|
|
1648
|
+
preservedPayloadFields.transcriptProvenance = updateMessageSourceReturnedCount(preservedPayloadFields.transcriptProvenance, sync.messages.length);
|
|
1649
|
+
}
|
|
1650
|
+
const returnedDebugReadChat = debugReadChat
|
|
1651
|
+
? {
|
|
1652
|
+
...debugReadChat,
|
|
1653
|
+
fullMsgCount: typeof debugReadChat.fullMsgCount === 'number'
|
|
1654
|
+
? debugReadChat.fullMsgCount
|
|
1655
|
+
: messages.length,
|
|
1656
|
+
visibleMsgCount: visibleMessages.length,
|
|
1657
|
+
hiddenMsgCount,
|
|
1658
|
+
returnedMsgCount: sync.messages.length,
|
|
1659
|
+
}
|
|
1660
|
+
: undefined;
|
|
1661
|
+
return {
|
|
1662
|
+
success: true,
|
|
1663
|
+
...validatedPayload,
|
|
1664
|
+
...preservedPayloadFields,
|
|
1665
|
+
messages: sync.messages,
|
|
1666
|
+
totalMessages: sync.totalMessages,
|
|
1667
|
+
...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
function toNonNegativeNumber(value: any): number {
|
|
1673
|
+
const numeric = Number(value ?? 0);
|
|
1674
|
+
return Number.isFinite(numeric) ? Math.max(0, numeric) : 0;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function getCliVisibleTranscriptCount(adapter: any): number {
|
|
1678
|
+
if (typeof adapter?.getScriptParsedStatus !== 'function') return 0;
|
|
1679
|
+
try {
|
|
1680
|
+
const parsed = parseMaybeJson(adapter.getScriptParsedStatus());
|
|
1681
|
+
return Array.isArray(parsed?.messages) ? parsed.messages.length : 0;
|
|
1682
|
+
} catch {
|
|
1683
|
+
return 0;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
export async function handleChatHistory(h: CommandHelpers, args: any): Promise<CommandResult> {
|
|
1688
|
+
const { agentType, offset, limit } = args;
|
|
1689
|
+
const historySessionId = getHistorySessionId(h, args);
|
|
1690
|
+
try {
|
|
1691
|
+
const provider = h.getProvider(agentType);
|
|
1692
|
+
const agentStr = provider?.type || agentType || getCurrentProviderType(h);
|
|
1693
|
+
const transport = getTargetTransport(h, provider);
|
|
1694
|
+
const hasExplicitExcludeRecentCount = args?.excludeRecentCount !== undefined && args?.excludeRecentCount !== null;
|
|
1695
|
+
let excludeRecentCount = toNonNegativeNumber(args?.excludeRecentCount);
|
|
1696
|
+
if (!hasExplicitExcludeRecentCount && isCliLikeTransport(transport)) {
|
|
1697
|
+
const adapter = getTargetedCliAdapter(h, args, provider?.type);
|
|
1698
|
+
const visibleCount = getCliVisibleTranscriptCount(adapter);
|
|
1699
|
+
if (visibleCount > excludeRecentCount) excludeRecentCount = visibleCount;
|
|
1700
|
+
}
|
|
1701
|
+
const workspace = typeof args?.workspace === 'string'
|
|
1702
|
+
? args.workspace
|
|
1703
|
+
: typeof (h.currentSession as any)?.workspace === 'string'
|
|
1704
|
+
? (h.currentSession as any).workspace
|
|
1705
|
+
: undefined;
|
|
1706
|
+
// Same runtime-fallback poison guard as the subscribe / history-only
|
|
1707
|
+
// paths (see resolveNativeHistoryReadSession): getHistorySessionId falls
|
|
1708
|
+
// back to targetSessionId (the ADHDev id) for an agy coordinator, and the
|
|
1709
|
+
// browser may also send that id back explicitly. Reading native history
|
|
1710
|
+
// keyed on it can never exact-bind (it is not the on-disk conv uuid). Drop
|
|
1711
|
+
// it here too so the pin / workspace-latest / owner-confirmed resolution
|
|
1712
|
+
// engages instead of fail-closing to pty-parser. A real DISTINCT provider
|
|
1713
|
+
// uuid is preserved.
|
|
1714
|
+
const {
|
|
1715
|
+
isRuntimeFallback: historySessionIdIsRuntimeFallback,
|
|
1716
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
|
|
1717
|
+
effectiveHistorySessionId,
|
|
1718
|
+
} = resolveNativeHistoryReadSession(args, historySessionId);
|
|
1719
|
+
const exactNativeHistoryScope = Boolean(
|
|
1720
|
+
(typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
|
|
1721
|
+
|| (typeof args?.historySessionId === 'string' && args.historySessionId.trim() && !historySessionIdIsRuntimeFallback)
|
|
1722
|
+
|| (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
|
|
1723
|
+
);
|
|
1724
|
+
const result = supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)
|
|
1725
|
+
? readCliProviderNativeHistory(agentStr, {
|
|
1726
|
+
canonicalHistory: provider?.nativeHistory,
|
|
1727
|
+
historySessionId: effectiveHistorySessionId,
|
|
1728
|
+
workspace,
|
|
1729
|
+
offset: offset || 0,
|
|
1730
|
+
limit: limit || 30,
|
|
1731
|
+
excludeRecentCount,
|
|
1732
|
+
historyBehavior: provider?.historyBehavior,
|
|
1733
|
+
scripts: provider?.scripts as any,
|
|
1734
|
+
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
1735
|
+
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
1736
|
+
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
1737
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
|
|
1738
|
+
allowWorkspaceLatestFallback: !pinnedProviderSessionIdForHistory && historySessionIdIsRuntimeFallback,
|
|
1739
|
+
})
|
|
1740
|
+
: readProviderChatHistory(agentStr, {
|
|
1741
|
+
canonicalHistory: provider?.nativeHistory,
|
|
1742
|
+
historySessionId,
|
|
1743
|
+
workspace,
|
|
1744
|
+
offset: offset || 0,
|
|
1745
|
+
limit: limit || 30,
|
|
1746
|
+
excludeRecentCount,
|
|
1747
|
+
historyBehavior: provider?.historyBehavior,
|
|
1748
|
+
scripts: provider?.scripts as any,
|
|
1749
|
+
});
|
|
1750
|
+
if (supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
|
|
1751
|
+
const lookup = (result as any).lookup === 'workspace' ? 'workspace' : 'session';
|
|
1752
|
+
const messages = Array.isArray((result as any).messages)
|
|
1753
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, (result as any).messages as ChatMessage[], (result as any)?.providerSessionId)
|
|
1754
|
+
: [];
|
|
1755
|
+
const historyProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
|
|
1756
|
+
? (result as any).providerSessionId
|
|
1757
|
+
: readHistorySessionIdFromMessages(messages) || effectiveHistorySessionId;
|
|
1758
|
+
// Mirror of the subscribe path (see handleReadChat): an antigravity
|
|
1759
|
+
// workspace-latest read still surfaces the on-disk uuid, but that uuid is
|
|
1760
|
+
// only safe to persist / trust when it was OWNER-token-confirmed as this
|
|
1761
|
+
// session's own — a bare recency pick could be a co-located replica's
|
|
1762
|
+
// conversation. Gate the pin and the same-pass identity on ownerConfirmed.
|
|
1763
|
+
const resolvedProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
|
|
1764
|
+
? (result as any).providerSessionId.trim()
|
|
1765
|
+
: '';
|
|
1766
|
+
const resultLookupIsWorkspace = lookup === 'workspace';
|
|
1767
|
+
const resultOwnerConfirmed = (result as any)?.ownerConfirmed === true;
|
|
1768
|
+
const ownerConfirmedUuid = resultOwnerConfirmed && typeof historyProviderSessionId === 'string' && historyProviderSessionId.trim()
|
|
1769
|
+
? historyProviderSessionId.trim()
|
|
1770
|
+
: '';
|
|
1771
|
+
if (resolvedProviderSessionId && (!resultLookupIsWorkspace || resultOwnerConfirmed)) {
|
|
1772
|
+
recordBoundProviderSessionId(h, effectiveReadSessionId(h, args?.targetSessionId), resolvedProviderSessionId);
|
|
1773
|
+
}
|
|
1774
|
+
const safeMapping = hasSafeNativeHistoryMapping({
|
|
1775
|
+
historySessionId: ownerConfirmedUuid || (lookup === 'workspace' ? undefined : effectiveHistorySessionId),
|
|
1776
|
+
providerSessionId: ownerConfirmedUuid || (lookup === 'workspace' ? undefined : historyProviderSessionId),
|
|
1777
|
+
workspace,
|
|
1778
|
+
nativeMessages: messages,
|
|
1779
|
+
});
|
|
1780
|
+
if ((result as any).source === 'provider-native' && messages.length > 0 && !safeMapping) {
|
|
1781
|
+
return {
|
|
1782
|
+
success: true,
|
|
1783
|
+
messages: [],
|
|
1784
|
+
hasMore: false,
|
|
1785
|
+
source: 'native-unavailable',
|
|
1786
|
+
agent: agentStr,
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return { success: true, ...result, agent: agentStr };
|
|
1791
|
+
} catch (e: any) {
|
|
1792
|
+
return { success: false, error: e.message };
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
|
|
1797
|
+
// Node scope guard: a daemon hosting a base node + several worktree nodes must
|
|
1798
|
+
// not serve worktree A's transcript (or splice sibling worktree turns via the
|
|
1799
|
+
// native-history-by-workspace fallback) when a coordinator scoped the read to
|
|
1800
|
+
// worktree B. mesh_read_chat always passes the requested node's workspace as
|
|
1801
|
+
// args.workspace; refuse a CONFIRMED cross-workspace read rather than mix.
|
|
1802
|
+
{
|
|
1803
|
+
const guardSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
1804
|
+
if (guardSessionId && typeof args?.workspace === 'string' && args.workspace.trim()) {
|
|
1805
|
+
const verdict = evaluateReadChatNodeWorkspaceScope({
|
|
1806
|
+
targetSessionId: guardSessionId,
|
|
1807
|
+
intendedWorkspace: args.workspace,
|
|
1808
|
+
sessionWorkspace: resolveTargetSessionActualWorkspace(h, guardSessionId),
|
|
1809
|
+
});
|
|
1810
|
+
if (verdict.scoped) {
|
|
1811
|
+
LOG.info('Command', `[read_chat] node scope mismatch: session ${guardSessionId} workspace "${verdict.actual}" ≠ requested node workspace "${verdict.intended}" — refusing cross-worktree transcript`);
|
|
1812
|
+
return {
|
|
1813
|
+
success: false,
|
|
1814
|
+
code: 'read_chat_session_node_scope_mismatch',
|
|
1815
|
+
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.`,
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
// Resolve provider in order: explicit agentType/providerType > registered session.
|
|
1821
|
+
// Without this fallback, callers that only have a sessionId (e.g. a chat tail
|
|
1822
|
+
// controller that just got handed a session ID over WS) get an empty result
|
|
1823
|
+
// because getProvider(undefined) returns undefined and the rest of the pipeline
|
|
1824
|
+
// bails. This makes the UI look like the session "disappeared".
|
|
1825
|
+
let providerHint: string | undefined = args?.agentType || args?.providerType;
|
|
1826
|
+
if (!providerHint) {
|
|
1827
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
1828
|
+
if (targetSessionId) {
|
|
1829
|
+
const session = (h.ctx as any)?.sessionRegistry?.get?.(targetSessionId);
|
|
1830
|
+
if (session && typeof session.providerType === 'string') {
|
|
1831
|
+
providerHint = session.providerType;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
if (!providerHint && h.currentSession?.providerType) {
|
|
1835
|
+
providerHint = h.currentSession.providerType;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const provider = h.getProvider(providerHint);
|
|
1839
|
+
const transport = getTargetTransport(h, provider);
|
|
1840
|
+
const historySessionId = getHistorySessionId(h, args);
|
|
1841
|
+
|
|
1842
|
+
const _log = (msg: string) => LOG.debug('Command', `[read_chat] ${msg}`);
|
|
1843
|
+
|
|
1844
|
+
// PTY / ACP transport: read from adapter
|
|
1845
|
+
if (isCliLikeTransport(transport)) {
|
|
1846
|
+
const adapter = getTargetedCliAdapter(h, args, provider?.type);
|
|
1847
|
+
if (adapter) {
|
|
1848
|
+
_log(`${transport} adapter: ${adapter.cliType}`);
|
|
1849
|
+
if (typeof adapter.getScriptParsedStatus !== 'function') {
|
|
1850
|
+
return { success: false, error: `${transport} adapter parseSession unavailable` };
|
|
1851
|
+
}
|
|
1852
|
+
let parsedStatus: any = null;
|
|
1853
|
+
try {
|
|
1854
|
+
parsedStatus = parseMaybeJson(adapter.getScriptParsedStatus());
|
|
1855
|
+
} catch (error: any) {
|
|
1856
|
+
return { success: false, error: error?.message || String(error) };
|
|
1857
|
+
}
|
|
1858
|
+
const parsedRecord = parsedStatus && typeof parsedStatus === 'object'
|
|
1859
|
+
? parsedStatus as Record<string, any>
|
|
1860
|
+
: null;
|
|
1861
|
+
if (!parsedRecord || !Array.isArray(parsedRecord.messages)) {
|
|
1862
|
+
return { success: false, error: `${transport} parser did not return messages` };
|
|
1863
|
+
}
|
|
1864
|
+
const adapterStatus = typeof adapter.getStatus === 'function'
|
|
1865
|
+
? adapter.getStatus()
|
|
1866
|
+
: {};
|
|
1867
|
+
const title = typeof parsedRecord.title === 'string' ? parsedRecord.title : undefined;
|
|
1868
|
+
const providerSessionId = typeof parsedRecord.providerSessionId === 'string'
|
|
1869
|
+
? parsedRecord.providerSessionId
|
|
1870
|
+
: undefined;
|
|
1871
|
+
const transcriptAuthority = parsedRecord.transcriptAuthority === 'provider' || parsedRecord.transcriptAuthority === 'daemon'
|
|
1872
|
+
? parsedRecord.transcriptAuthority
|
|
1873
|
+
: undefined;
|
|
1874
|
+
const coverage = parsedRecord.coverage === 'full' || parsedRecord.coverage === 'tail' || parsedRecord.coverage === 'current-turn'
|
|
1875
|
+
? parsedRecord.coverage
|
|
1876
|
+
: undefined;
|
|
1877
|
+
const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
|
|
1878
|
+
const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus, parsedRecord.messages);
|
|
1879
|
+
const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
|
|
1880
|
+
const parsedMessages = collapseAdjacentDuplicateChatMessages(
|
|
1881
|
+
finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus),
|
|
1882
|
+
);
|
|
1883
|
+
const returnedMessages = runtimeMessageMerger?.category === 'cli'
|
|
1884
|
+
&& runtimeMessageMerger.type === adapter.cliType
|
|
1885
|
+
&& typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
|
|
1886
|
+
? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
|
|
1887
|
+
: parsedMessages;
|
|
1888
|
+
const providerType = provider?.type || adapter.cliType;
|
|
1889
|
+
let selectedMessages = returnedMessages;
|
|
1890
|
+
let selectedTitle = title;
|
|
1891
|
+
let selectedProviderSessionId = providerSessionId;
|
|
1892
|
+
let selectedTranscriptAuthority = transcriptAuthority;
|
|
1893
|
+
let selectedCoverage = coverage;
|
|
1894
|
+
let selectedStatus = returnedStatus;
|
|
1895
|
+
const _targetSidForWs = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
1896
|
+
const _registryWs = _targetSidForWs
|
|
1897
|
+
? (h.ctx?.sessionRegistry?.get?.(_targetSidForWs) as any)?.workspace
|
|
1898
|
+
: undefined;
|
|
1899
|
+
const _currentSessionWs = typeof (h.currentSession as any)?.workspace === 'string'
|
|
1900
|
+
? (h.currentSession as any).workspace
|
|
1901
|
+
: typeof adapter.workingDir === 'string'
|
|
1902
|
+
? adapter.workingDir
|
|
1903
|
+
: undefined;
|
|
1904
|
+
const sessionWorkspace = _targetSidForWs
|
|
1905
|
+
? (typeof _registryWs === 'string' ? _registryWs : (typeof args?.workspace === 'string' ? args.workspace : undefined) ?? _currentSessionWs)
|
|
1906
|
+
: _currentSessionWs;
|
|
1907
|
+
const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
|
|
1908
|
+
// ───────────────────────────────────────────────────────────
|
|
1909
|
+
// Chat source decision via ChatSourceMachine (A2 big-bang).
|
|
1910
|
+
// Replaces the ~300-line if-ladder that mixed source decision
|
|
1911
|
+
// with native fetch, anchor mutation, and runtime mirror
|
|
1912
|
+
// selection. The machine decides only between native-history
|
|
1913
|
+
// and pty-parser; downstream selection of which message array
|
|
1914
|
+
// to surface stays here.
|
|
1915
|
+
//
|
|
1916
|
+
// Behavioural changes vs v1:
|
|
1917
|
+
// - No more nativeHistoryAnchoredAt mutation on the adapter.
|
|
1918
|
+
// Lock state lives in CHAT_SOURCE_REGISTRY keyed by
|
|
1919
|
+
// (providerType, sessionId).
|
|
1920
|
+
// - No PTY-vs-native freshness comparison. The lock holds
|
|
1921
|
+
// across arbitrary PTY arrival; only native regression /
|
|
1922
|
+
// unavailability unlocks. This is the plipping fix.
|
|
1923
|
+
// - 6 trigger strings (native_history_partial / _stale /
|
|
1924
|
+
// _not_safely_mapped / _empty / _error / _unavailable)
|
|
1925
|
+
// collapse to 3 events with diagnostic causes preserved
|
|
1926
|
+
// and mapped back to legacy fallbackReason strings for
|
|
1927
|
+
// response compatibility.
|
|
1928
|
+
// - Codex live-workspace native probe and unsafe-native
|
|
1929
|
+
// daemon mirror fallbacks are preserved as additional
|
|
1930
|
+
// input rounds to the machine; they were never the source
|
|
1931
|
+
// decision itself, they were retries.
|
|
1932
|
+
// ───────────────────────────────────────────────────────────
|
|
1933
|
+
|
|
1934
|
+
const supportsNative = supportsCliNativeTranscript(providerType, provider)
|
|
1935
|
+
&& isNativeSourceCanonicalHistory(provider?.nativeHistory);
|
|
1936
|
+
const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
|
|
1937
|
+
const workspace = sessionWorkspace;
|
|
1938
|
+
const nativeHistoryLimit = Math.max(
|
|
1939
|
+
normalizeReadChatTailLimit(args) || 0,
|
|
1940
|
+
returnedMessages.length,
|
|
1941
|
+
HOT_TAIL_MIN_LIMIT,
|
|
1942
|
+
);
|
|
1943
|
+
const nativeHistorySessionId = supportsNative
|
|
1944
|
+
? resolveCliNativeHistorySessionId(args, historySessionId, providerSessionId)
|
|
1945
|
+
: undefined;
|
|
1946
|
+
const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
1947
|
+
const skipLiveNativeHistoryWithoutProviderSession = shouldSkipLiveCliNativeHistoryWithoutProviderSession({
|
|
1948
|
+
adapter,
|
|
1949
|
+
providerType,
|
|
1950
|
+
readChatArgs: args,
|
|
1951
|
+
nativeHistorySessionId,
|
|
1952
|
+
parsedProviderSessionId: providerSessionId,
|
|
1953
|
+
});
|
|
1954
|
+
const nativeHistoryReadSessionId = skipLiveNativeHistoryWithoutProviderSession
|
|
1955
|
+
? undefined
|
|
1956
|
+
: nativeHistorySessionId;
|
|
1957
|
+
const exactNativeHistoryScope = Boolean(
|
|
1958
|
+
(typeof args?.historySessionId === 'string' && args.historySessionId.trim())
|
|
1959
|
+
|| (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
|
|
1960
|
+
|| providerSessionId
|
|
1961
|
+
|| (nativeHistoryReadSessionId && nativeHistoryReadSessionId !== targetSessionId)
|
|
1962
|
+
|| ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
|
|
1963
|
+
);
|
|
1964
|
+
|
|
1965
|
+
// 1. Fetch native history (or skip if provider does not support it).
|
|
1966
|
+
let nativeHistory: any | null = null;
|
|
1967
|
+
let nativeHistoryError: unknown | undefined;
|
|
1968
|
+
if (supportsNative) {
|
|
1969
|
+
// Runtime-fallback → pin substitution (see
|
|
1970
|
+
// resolveNativeHistoryReadSession): nativeHistoryReadSessionId is
|
|
1971
|
+
// the bare runtime/session id when no explicit provider handle was
|
|
1972
|
+
// supplied and none was parsed (antigravity takes no --session-id, so
|
|
1973
|
+
// its this.providerSessionId stays empty and getHistorySessionId falls
|
|
1974
|
+
// back to targetSessionId). That runtime id is not the on-disk
|
|
1975
|
+
// conversations/<uuid>.db name, so a native read keyed on it can never
|
|
1976
|
+
// exact-bind and falls to the recency heuristic — which drops an idle
|
|
1977
|
+
// (or restored, spawnedAtMs=0) session's own store. Prefer a pin (a
|
|
1978
|
+
// real conversation id a prior read resolved for THIS session, now also
|
|
1979
|
+
// persisted across restart) over the runtime id, else drop the runtime
|
|
1980
|
+
// id so readCliProviderNativeHistory's pin / workspace-latest paths can
|
|
1981
|
+
// engage. Mirrors the handleChatHistory path's established handling.
|
|
1982
|
+
const {
|
|
1983
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForRead,
|
|
1984
|
+
effectiveHistorySessionId: effectiveNativeReadSessionId,
|
|
1985
|
+
} = resolveNativeHistoryReadSession(args, nativeHistoryReadSessionId);
|
|
1986
|
+
try {
|
|
1987
|
+
nativeHistory = readCliProviderNativeHistory(agentStr, {
|
|
1988
|
+
canonicalHistory: provider?.nativeHistory,
|
|
1989
|
+
historySessionId: effectiveNativeReadSessionId,
|
|
1990
|
+
workspace,
|
|
1991
|
+
offset: 0,
|
|
1992
|
+
limit: nativeHistoryLimit,
|
|
1993
|
+
excludeRecentCount: 0,
|
|
1994
|
+
historyBehavior: provider?.historyBehavior,
|
|
1995
|
+
scripts: provider?.scripts as any,
|
|
1996
|
+
excludeInProgressTurn: returnedStatus === 'waiting_approval',
|
|
1997
|
+
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
1998
|
+
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
1999
|
+
// Stable per-session identity for antigravity's conversation-claim
|
|
2000
|
+
// owner token (== session registry sessionId == instance instanceId).
|
|
2001
|
+
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
2002
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForRead,
|
|
2003
|
+
// Last-resort only when no pin was ever recorded for this
|
|
2004
|
+
// session; the downstream workspace-overlap safety gate
|
|
2005
|
+
// still filters an aliased session out.
|
|
2006
|
+
allowWorkspaceLatestFallback: !pinnedProviderSessionIdForRead,
|
|
2007
|
+
});
|
|
2008
|
+
// Refresh the per-mesh-session pin whenever a native read
|
|
2009
|
+
// resolves a concrete provider-native session id. A later
|
|
2010
|
+
// post-turn read (live binding gone) can then reuse it
|
|
2011
|
+
// instead of fail-closing. Only a non-empty resolved id
|
|
2012
|
+
// updates the pin; an empty result never clears a known one.
|
|
2013
|
+
const resolvedProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
|
|
2014
|
+
? nativeHistory.providerSessionId.trim()
|
|
2015
|
+
: '';
|
|
2016
|
+
// Pin gating for the antigravity workspace-latest branch: a
|
|
2017
|
+
// coordinator session has no pin (agy takes no --session-id) and
|
|
2018
|
+
// spawnedAtMs=0 after attach-restore, so the read resolves via the
|
|
2019
|
+
// workspace-latest fallback (lookup === 'workspace') rather than an
|
|
2020
|
+
// exact bind. The dispatcher STILL surfaces the on-disk conversation
|
|
2021
|
+
// uuid there — but that uuid is only safe to persist as a pin when it
|
|
2022
|
+
// was OWNER-token-confirmed (exact uuid bind, or a spawn-floor/birth
|
|
2023
|
+
// pick). A bare recency/newest-by-mtime pick (ownerConfirmed=false)
|
|
2024
|
+
// could be a co-located replica's conversation, so recording it would
|
|
2025
|
+
// hard-wire the coordinator↔replica crosswire permanently — never pin
|
|
2026
|
+
// that. Exact-bind / session-scoped reads (lookup === 'session') are
|
|
2027
|
+
// already owner-scoped by construction, so keep pinning them as before.
|
|
2028
|
+
const resolvedLookupIsWorkspace = (nativeHistory as any)?.lookup === 'workspace';
|
|
2029
|
+
const nativeOwnerConfirmed = (nativeHistory as any)?.ownerConfirmed === true;
|
|
2030
|
+
const mayPinResolvedProviderSessionId = resolvedProviderSessionId
|
|
2031
|
+
&& (!resolvedLookupIsWorkspace || nativeOwnerConfirmed);
|
|
2032
|
+
if (mayPinResolvedProviderSessionId) {
|
|
2033
|
+
recordBoundProviderSessionId(h, effectiveReadSessionId(h, targetSessionId), resolvedProviderSessionId);
|
|
2034
|
+
}
|
|
2035
|
+
} catch (error: any) {
|
|
2036
|
+
nativeHistoryError = error;
|
|
2037
|
+
nativeHistory = null;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// 2. Compute safeMapping with the same rules the v1 code used so the
|
|
2042
|
+
// machine sees the same observation it always would have.
|
|
2043
|
+
let nativeMessages: ChatMessage[] = nativeHistory && Array.isArray(nativeHistory.messages)
|
|
2044
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
|
|
2045
|
+
: [];
|
|
2046
|
+
const sessionStartedAtMs = sessionStartedAtMsFromRegistry(h, args?.targetSessionId);
|
|
2047
|
+
let historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
|
|
2048
|
+
? nativeHistory.providerSessionId
|
|
2049
|
+
: readHistorySessionIdFromMessages(nativeMessages) || nativeHistoryReadSessionId || historySessionId;
|
|
2050
|
+
let lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
|
|
2051
|
+
// Owner-confirmed uuid for THIS read (antigravity): the dispatcher
|
|
2052
|
+
// resolved a conversation and confirmed it is this session's own via the
|
|
2053
|
+
// owner token (exact uuid bind, or a spawn-floor/birth pick) — NOT a bare
|
|
2054
|
+
// recency pick. When present it is the authoritative conversation
|
|
2055
|
+
// identity for the same-pass safe-mapping check below, even on a
|
|
2056
|
+
// workspace-latest (lookup === 'workspace') read where the coordinator
|
|
2057
|
+
// has no pin. A coordinator session hits this path (agy takes no
|
|
2058
|
+
// --session-id, spawnedAtMs=0 after attach-restore); without it the
|
|
2059
|
+
// safe-mapping check saw undefined identity → workspace-overlap branch →
|
|
2060
|
+
// the PTY snapshot has only the user echo → fail-closed → regress to
|
|
2061
|
+
// pty-parser (user-echo only). Trusting the owner-confirmed uuid lets the
|
|
2062
|
+
// assistant answer reach the dashboard on the FIRST read.
|
|
2063
|
+
const ownerConfirmedUuid = adapter.cliType === 'antigravity-cli'
|
|
2064
|
+
&& (nativeHistory as any)?.ownerConfirmed === true
|
|
2065
|
+
&& typeof historyProviderSessionId === 'string'
|
|
2066
|
+
&& historyProviderSessionId.trim()
|
|
2067
|
+
? historyProviderSessionId.trim()
|
|
2068
|
+
: '';
|
|
2069
|
+
let nativeHistorySessionForMapping = ownerConfirmedUuid
|
|
2070
|
+
? ownerConfirmedUuid
|
|
2071
|
+
: adapter.cliType === 'antigravity-cli'
|
|
2072
|
+
&& historyProviderSessionId
|
|
2073
|
+
&& nativeHistoryReadSessionId
|
|
2074
|
+
&& historyProviderSessionId !== nativeHistoryReadSessionId
|
|
2075
|
+
? undefined
|
|
2076
|
+
: nativeHistoryReadSessionId;
|
|
2077
|
+
// For an owner-confirmed uuid, feed the uuid as the explicit session
|
|
2078
|
+
// identity to the safe-mapping check even on a workspace-latest read so
|
|
2079
|
+
// the session-branch identity test runs uuid-to-uuid (messages carry the
|
|
2080
|
+
// uuid as historySessionId) and trusts the assistant in this same pass.
|
|
2081
|
+
let safeMapping = supportsNative && nativeHistory
|
|
2082
|
+
? hasSafeNativeHistoryMapping({
|
|
2083
|
+
historySessionId: ownerConfirmedUuid || (lookup === 'workspace' ? undefined : nativeHistorySessionForMapping),
|
|
2084
|
+
providerSessionId: ownerConfirmedUuid || (lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId),
|
|
2085
|
+
workspace,
|
|
2086
|
+
nativeMessages,
|
|
2087
|
+
ptyMessages: returnedMessages,
|
|
2088
|
+
requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope && !ownerConfirmedUuid,
|
|
2089
|
+
})
|
|
2090
|
+
: false;
|
|
2091
|
+
if (skipLiveNativeHistoryWithoutProviderSession && (!safeMapping || returnedMessages.length === 0)) {
|
|
2092
|
+
nativeHistory = null;
|
|
2093
|
+
nativeMessages = [];
|
|
2094
|
+
historyProviderSessionId = undefined;
|
|
2095
|
+
lookup = 'session';
|
|
2096
|
+
safeMapping = false;
|
|
2097
|
+
}
|
|
2098
|
+
const mayRetryUnsafeAutoDetectedCodexSession = adapter.cliType === 'codex-cli'
|
|
2099
|
+
&& !getExplicitHistorySessionId(args)
|
|
2100
|
+
&& Boolean(sessionStartedAtMs && sessionStartedAtMs > 0)
|
|
2101
|
+
&& !skipLiveNativeHistoryWithoutProviderSession
|
|
2102
|
+
&& !safeMapping;
|
|
2103
|
+
if (mayRetryUnsafeAutoDetectedCodexSession) {
|
|
2104
|
+
try {
|
|
2105
|
+
nativeHistory = readCliProviderNativeHistory(agentStr, {
|
|
2106
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2107
|
+
historySessionId: undefined,
|
|
2108
|
+
workspace,
|
|
2109
|
+
offset: 0,
|
|
2110
|
+
limit: nativeHistoryLimit,
|
|
2111
|
+
excludeRecentCount: 0,
|
|
2112
|
+
historyBehavior: provider?.historyBehavior,
|
|
2113
|
+
scripts: provider?.scripts as any,
|
|
2114
|
+
excludeInProgressTurn: returnedStatus === 'waiting_approval',
|
|
2115
|
+
sessionStartedAtMs,
|
|
2116
|
+
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
2117
|
+
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
2118
|
+
});
|
|
2119
|
+
nativeHistoryError = undefined;
|
|
2120
|
+
nativeMessages = nativeHistory && Array.isArray(nativeHistory.messages)
|
|
2121
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
|
|
2122
|
+
: [];
|
|
2123
|
+
historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
|
|
2124
|
+
? nativeHistory.providerSessionId
|
|
2125
|
+
: readHistorySessionIdFromMessages(nativeMessages);
|
|
2126
|
+
lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
|
|
2127
|
+
nativeHistorySessionForMapping = undefined;
|
|
2128
|
+
safeMapping = supportsNative && nativeHistory
|
|
2129
|
+
? hasSafeNativeHistoryMapping({
|
|
2130
|
+
historySessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
|
|
2131
|
+
providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
|
|
2132
|
+
workspace,
|
|
2133
|
+
nativeMessages,
|
|
2134
|
+
ptyMessages: returnedMessages,
|
|
2135
|
+
requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
|
|
2136
|
+
})
|
|
2137
|
+
: false;
|
|
2138
|
+
} catch (error: any) {
|
|
2139
|
+
nativeHistoryError = error;
|
|
2140
|
+
nativeHistory = null;
|
|
2141
|
+
nativeMessages = [];
|
|
2142
|
+
historyProviderSessionId = undefined;
|
|
2143
|
+
safeMapping = false;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const trustedExactNativeIdentity = lookup !== 'workspace'
|
|
2147
|
+
&& Boolean(nativeHistoryReadSessionId)
|
|
2148
|
+
&& Boolean(historyProviderSessionId)
|
|
2149
|
+
&& nativeHistoryReadSessionId === historyProviderSessionId;
|
|
2150
|
+
|
|
2151
|
+
// 3. Drive ChatSourceMachine — one observation per readChat call,
|
|
2152
|
+
// keyed by (providerType, sessionKey-for-this-call). targetSessionId
|
|
2153
|
+
// is the most specific session anchor we have; fall back to
|
|
2154
|
+
// historySessionId so we never leak state across distinct sessions.
|
|
2155
|
+
const machineSessionKey = String(
|
|
2156
|
+
args?.targetSessionId
|
|
2157
|
+
|| providerSessionId
|
|
2158
|
+
|| historySessionId
|
|
2159
|
+
|| (h.currentSession as any)?.sessionId
|
|
2160
|
+
|| ''
|
|
2161
|
+
);
|
|
2162
|
+
const primary = decideCliReadChatSource({
|
|
2163
|
+
providerType,
|
|
2164
|
+
provider,
|
|
2165
|
+
sessionId: machineSessionKey,
|
|
2166
|
+
nativeHistoryResult: nativeHistory,
|
|
2167
|
+
nativeHistoryError,
|
|
2168
|
+
safeMapping,
|
|
2169
|
+
trustedExactNativeIdentity,
|
|
2170
|
+
sessionWorkspace,
|
|
2171
|
+
intendedWorkspace,
|
|
2172
|
+
ptyMessages: returnedMessages,
|
|
2173
|
+
// Start with PTY visible; decideCliReadChatSource flips this
|
|
2174
|
+
// to true when the machine actually selects native-history.
|
|
2175
|
+
ptyStatusApprovalOnly: false,
|
|
2176
|
+
});
|
|
2177
|
+
let messageSource: Record<string, unknown> = primary.messageSource;
|
|
2178
|
+
|
|
2179
|
+
if (primary.nativeSelected) {
|
|
2180
|
+
selectedMessages = finalizeStreamingMessagesWhenIdle(primary.nativeMessages, returnedStatus);
|
|
2181
|
+
selectedProviderSessionId = historyProviderSessionId || providerSessionId;
|
|
2182
|
+
selectedTranscriptAuthority = 'provider';
|
|
2183
|
+
selectedCoverage = nativeHistory?.hasMore ? 'tail' : 'full';
|
|
2184
|
+
if (selectedProviderSessionId && selectedProviderSessionId !== providerSessionId) {
|
|
2185
|
+
adapter.updateRuntimeMeta?.({ providerSessionId: selectedProviderSessionId });
|
|
2186
|
+
}
|
|
2187
|
+
} else if (supportsNative) {
|
|
2188
|
+
// Native not selected. Two preserved v1 fallbacks before settling
|
|
2189
|
+
// on PTY: (a) Codex-only live workspace native probe; (b) unsafe-
|
|
2190
|
+
// native daemon mirror selection. The machine sees each retry as
|
|
2191
|
+
// an additional observation.
|
|
2192
|
+
const liveCurrentRuntimePtySafe = isCurrentRuntimePtySafelyAttributed({
|
|
2193
|
+
adapter,
|
|
2194
|
+
helpers: h,
|
|
2195
|
+
readChatArgs: args,
|
|
2196
|
+
sessionWorkspace,
|
|
2197
|
+
intendedWorkspace,
|
|
2198
|
+
ptyMessages: returnedMessages,
|
|
2199
|
+
});
|
|
2200
|
+
const mayProbeLiveCodexWorkspaceNative = adapter.cliType === 'codex-cli'
|
|
2201
|
+
&& liveCurrentRuntimePtySafe
|
|
2202
|
+
&& !(typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
|
|
2203
|
+
&& !(providerSessionId && providerSessionId.trim())
|
|
2204
|
+
&& !nativeHistoryReadSessionId
|
|
2205
|
+
&& (!historyProviderSessionId || historyProviderSessionId === nativeHistoryReadSessionId || historyProviderSessionId === historySessionId)
|
|
2206
|
+
&& !skipLiveNativeHistoryWithoutProviderSession;
|
|
2207
|
+
const liveWorkspaceNativeHistory = mayProbeLiveCodexWorkspaceNative
|
|
2208
|
+
? readLiveCodexWorkspaceNativeHistory(agentStr, {
|
|
2209
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2210
|
+
workspace,
|
|
2211
|
+
offset: 0,
|
|
2212
|
+
limit: nativeHistoryLimit,
|
|
2213
|
+
excludeRecentCount: 0,
|
|
2214
|
+
historyBehavior: provider?.historyBehavior,
|
|
2215
|
+
scripts: provider?.scripts as any,
|
|
2216
|
+
})
|
|
2217
|
+
: null;
|
|
2218
|
+
const liveWorkspaceNativeMessages = Array.isArray((liveWorkspaceNativeHistory as any)?.messages)
|
|
2219
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, (liveWorkspaceNativeHistory as any).messages as ChatMessage[], (liveWorkspaceNativeHistory as any)?.providerSessionId)
|
|
2220
|
+
: [];
|
|
2221
|
+
const liveWorkspaceNativeProviderSessionId = typeof (liveWorkspaceNativeHistory as any)?.providerSessionId === 'string'
|
|
2222
|
+
? (liveWorkspaceNativeHistory as any).providerSessionId
|
|
2223
|
+
: readHistorySessionIdFromMessages(liveWorkspaceNativeMessages);
|
|
2224
|
+
const liveWorkspaceNativeSafeMapping = liveWorkspaceNativeMessages.length > 0
|
|
2225
|
+
&& hasSafeNativeHistoryMapping({
|
|
2226
|
+
workspace,
|
|
2227
|
+
nativeMessages: liveWorkspaceNativeMessages,
|
|
2228
|
+
ptyMessages: returnedMessages,
|
|
2229
|
+
requireWorkspaceContentOverlap: true,
|
|
2230
|
+
});
|
|
2231
|
+
if (liveWorkspaceNativeHistory) {
|
|
2232
|
+
const liveDecision = decideCliReadChatSource({
|
|
2233
|
+
providerType,
|
|
2234
|
+
provider,
|
|
2235
|
+
// Distinct session key so a transient codex live-probe does not
|
|
2236
|
+
// clobber the primary session's lock. The machine treats this
|
|
2237
|
+
// as its own session; the primary session's state is untouched.
|
|
2238
|
+
sessionId: `${machineSessionKey}::live-workspace`,
|
|
2239
|
+
nativeHistoryResult: liveWorkspaceNativeHistory,
|
|
2240
|
+
safeMapping: liveWorkspaceNativeSafeMapping,
|
|
2241
|
+
sessionWorkspace,
|
|
2242
|
+
intendedWorkspace,
|
|
2243
|
+
ptyMessages: returnedMessages,
|
|
2244
|
+
ptyStatusApprovalOnly: true,
|
|
2245
|
+
});
|
|
2246
|
+
if (liveDecision.nativeSelected) {
|
|
2247
|
+
selectedMessages = finalizeStreamingMessagesWhenIdle(liveDecision.nativeMessages, returnedStatus);
|
|
2248
|
+
selectedProviderSessionId = liveWorkspaceNativeProviderSessionId || providerSessionId;
|
|
2249
|
+
selectedTranscriptAuthority = 'provider';
|
|
2250
|
+
selectedCoverage = (liveWorkspaceNativeHistory as any).hasMore ? 'tail' : 'full';
|
|
2251
|
+
if (selectedProviderSessionId && selectedProviderSessionId !== providerSessionId) {
|
|
2252
|
+
adapter.updateRuntimeMeta?.({ providerSessionId: selectedProviderSessionId });
|
|
2253
|
+
}
|
|
2254
|
+
messageSource = liveDecision.messageSource;
|
|
2255
|
+
(messageSource as any).selectedDaemonSource = 'live-workspace-native-history';
|
|
2256
|
+
(messageSource as any).runtimeMappingSafe = true;
|
|
2257
|
+
} else {
|
|
2258
|
+
// Live probe also rejected: apply unsafe-native daemon mirror
|
|
2259
|
+
// selection (codex-only) using the primary decision's
|
|
2260
|
+
// fallbackReason.
|
|
2261
|
+
applyUnsafeNativeDaemonFallback({
|
|
2262
|
+
providerType,
|
|
2263
|
+
adapter,
|
|
2264
|
+
helpers: h,
|
|
2265
|
+
readChatArgs: args,
|
|
2266
|
+
sessionWorkspace,
|
|
2267
|
+
intendedWorkspace,
|
|
2268
|
+
ptyMessages: returnedMessages,
|
|
2269
|
+
nativeHistoryLimit,
|
|
2270
|
+
provider,
|
|
2271
|
+
messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
|
|
2272
|
+
apply(selection) {
|
|
2273
|
+
selectedMessages = selection.messages;
|
|
2274
|
+
selectedTranscriptAuthority = selection.transcriptAuthority;
|
|
2275
|
+
selectedCoverage = selection.coverage ?? coverage;
|
|
2276
|
+
selectedStatus = selection.status ?? returnedStatus;
|
|
2277
|
+
},
|
|
2278
|
+
activeModal,
|
|
2279
|
+
returnedStatus,
|
|
2280
|
+
coverage,
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
} else {
|
|
2284
|
+
applyUnsafeNativeDaemonFallback({
|
|
2285
|
+
providerType,
|
|
2286
|
+
adapter,
|
|
2287
|
+
helpers: h,
|
|
2288
|
+
readChatArgs: args,
|
|
2289
|
+
sessionWorkspace,
|
|
2290
|
+
intendedWorkspace,
|
|
2291
|
+
ptyMessages: returnedMessages,
|
|
2292
|
+
nativeHistoryLimit,
|
|
2293
|
+
provider,
|
|
2294
|
+
messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
|
|
2295
|
+
apply(selection) {
|
|
2296
|
+
selectedMessages = selection.messages;
|
|
2297
|
+
selectedTranscriptAuthority = selection.transcriptAuthority;
|
|
2298
|
+
selectedCoverage = selection.coverage ?? coverage;
|
|
2299
|
+
selectedStatus = selection.status ?? returnedStatus;
|
|
2300
|
+
},
|
|
2301
|
+
activeModal,
|
|
2302
|
+
returnedStatus,
|
|
2303
|
+
coverage,
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
if (
|
|
2308
|
+
isGeneratingLikeStatus(selectedStatus)
|
|
2309
|
+
&& selectedTranscriptAuthority === 'provider'
|
|
2310
|
+
&& !hasNonEmptyModalButtons(activeModal)
|
|
2311
|
+
&& hasFinalVisibleAssistantMessage(selectedMessages)
|
|
2312
|
+
) {
|
|
2313
|
+
selectedStatus = 'idle';
|
|
2314
|
+
selectedMessages = finalizeStreamingMessagesWhenIdle(selectedMessages, selectedStatus);
|
|
2315
|
+
messageSource = {
|
|
2316
|
+
...messageSource,
|
|
2317
|
+
statusReconciled: {
|
|
2318
|
+
from: returnedStatus,
|
|
2319
|
+
to: 'idle',
|
|
2320
|
+
reason: 'provider_native_final_assistant',
|
|
2321
|
+
},
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
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}`);
|
|
2325
|
+
return buildReadChatCommandResult({
|
|
2326
|
+
messages: selectedMessages,
|
|
2327
|
+
status: selectedStatus,
|
|
2328
|
+
activeModal,
|
|
2329
|
+
messageSource,
|
|
2330
|
+
transcriptProvenance: messageSource,
|
|
2331
|
+
debugReadChat: {
|
|
2332
|
+
provider: adapter.cliType,
|
|
2333
|
+
targetSessionId: String(args?.targetSessionId || ''),
|
|
2334
|
+
adapterStatus: String(adapterStatus.status || ''),
|
|
2335
|
+
parsedStatus: String(parsedRecord.status || ''),
|
|
2336
|
+
returnedStatus: String(selectedStatus || ''),
|
|
2337
|
+
selectedMessageSource: (messageSource as any).selected,
|
|
2338
|
+
messageSource,
|
|
2339
|
+
shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType, provider)
|
|
2340
|
+
&& isNativeSourceCanonicalHistory(provider?.nativeHistory)
|
|
2341
|
+
&& (messageSource as any).selected !== 'native-history'
|
|
2342
|
+
&& typeof (messageSource as any).fallbackReason === 'string'
|
|
2343
|
+
&& (messageSource as any).fallbackReason.startsWith('native_history_')
|
|
2344
|
+
&& (messageSource as any).fallbackReason !== 'native_history_not_checked'
|
|
2345
|
+
&& !isUnsafeNativeTranscriptFallback((messageSource as any).fallbackReason)
|
|
2346
|
+
&& !(selectedTranscriptAuthority === 'provider' && selectedCoverage === 'full'),
|
|
2347
|
+
parsedMsgCount: parsedRecord.messages.length,
|
|
2348
|
+
returnedMsgCount: selectedMessages.length,
|
|
2349
|
+
},
|
|
2350
|
+
...(selectedTitle ? { title: selectedTitle } : {}),
|
|
2351
|
+
...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
|
|
2352
|
+
...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
|
|
2353
|
+
...(selectedCoverage ? { coverage: selectedCoverage } : {}),
|
|
2354
|
+
}, args, h);
|
|
2355
|
+
}
|
|
2356
|
+
// History-only path (no adapter). Same source-decision contract as
|
|
2357
|
+
// the adapter path above, but with no PTY messages — the machine
|
|
2358
|
+
// simply decides whether native is usable; if not we return the
|
|
2359
|
+
// history we have plus a `native_history_not_safely_available`
|
|
2360
|
+
// error response when the provider requires native source.
|
|
2361
|
+
const historyLimit = normalizeReadChatTailLimit(args);
|
|
2362
|
+
try {
|
|
2363
|
+
const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h);
|
|
2364
|
+
const targetSid = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
2365
|
+
const registrySessionWorkspace = targetSid
|
|
2366
|
+
? (h.ctx?.sessionRegistry?.get?.(targetSid) as any)?.workspace
|
|
2367
|
+
: undefined;
|
|
2368
|
+
const currentSessionWorkspace = typeof (h.currentSession as any)?.workspace === 'string'
|
|
2369
|
+
? (h.currentSession as any).workspace
|
|
2370
|
+
: undefined;
|
|
2371
|
+
const argsWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
|
|
2372
|
+
// When reading a different session (targetSid), prefer that session's registered
|
|
2373
|
+
// workspace (or the caller-supplied args.workspace) over the current (coordinator)
|
|
2374
|
+
// session's workspace — otherwise the coordinator's cwd shadows the worker's cwd
|
|
2375
|
+
// and history lookups find the wrong files.
|
|
2376
|
+
const workspace = targetSid
|
|
2377
|
+
? (typeof registrySessionWorkspace === 'string' ? registrySessionWorkspace : argsWorkspace ?? currentSessionWorkspace)
|
|
2378
|
+
: (typeof currentSessionWorkspace === 'string' ? currentSessionWorkspace : undefined);
|
|
2379
|
+
const intendedWorkspace = argsWorkspace;
|
|
2380
|
+
const supportsNative = supportsCliNativeTranscript(agentStr, provider)
|
|
2381
|
+
&& isNativeSourceCanonicalHistory(provider?.nativeHistory);
|
|
2382
|
+
// Post-turn read (no live adapter): getHistorySessionId falls back to
|
|
2383
|
+
// the daemon runtime session id when no provider-native id was ever
|
|
2384
|
+
// registered. That runtime id is NOT a real provider session, so a
|
|
2385
|
+
// native read keyed on it resolves nothing (providerSessionId=null,
|
|
2386
|
+
// zero rows) even though the transcript is present in state.db. When
|
|
2387
|
+
// this is that runtime fallback (historySessionId === targetSid and no
|
|
2388
|
+
// explicit id was passed) and we hold a pin from an earlier bound
|
|
2389
|
+
// read, prefer the pin so the query hits the real session. Detects the
|
|
2390
|
+
// fallback whether historySessionId reached targetSid via
|
|
2391
|
+
// getHistorySessionId's internal fallback (empty args) or the browser
|
|
2392
|
+
// explicitly echoed targetSid back (poisoned agy-coordinator
|
|
2393
|
+
// subscription / D8 refreshAuthoritativeTail read); a real DISTINCT
|
|
2394
|
+
// provider uuid still exact-binds unchanged. See
|
|
2395
|
+
// resolveNativeHistoryReadSession.
|
|
2396
|
+
const {
|
|
2397
|
+
isRuntimeFallback: historySessionIdIsRuntimeFallback,
|
|
2398
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
|
|
2399
|
+
effectiveHistorySessionId: effectiveHistorySessionIdForRead,
|
|
2400
|
+
} = resolveNativeHistoryReadSession(args, historySessionId);
|
|
2401
|
+
const history = supportsNative
|
|
2402
|
+
? readCliProviderNativeHistory(agentStr, {
|
|
2403
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2404
|
+
historySessionId: effectiveHistorySessionIdForRead,
|
|
2405
|
+
workspace,
|
|
2406
|
+
offset: 0,
|
|
2407
|
+
limit: historyLimit,
|
|
2408
|
+
excludeRecentCount: 0,
|
|
2409
|
+
historyBehavior: provider?.historyBehavior,
|
|
2410
|
+
scripts: provider?.scripts as any,
|
|
2411
|
+
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
2412
|
+
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
2413
|
+
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
2414
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
|
|
2415
|
+
// Last-resort only when no pin was ever recorded AND the
|
|
2416
|
+
// runtime fallback did not resolve a real provider session.
|
|
2417
|
+
allowWorkspaceLatestFallback: !pinnedProviderSessionIdForHistory && historySessionIdIsRuntimeFallback,
|
|
2418
|
+
})
|
|
2419
|
+
: readProviderChatHistory(agentStr, {
|
|
2420
|
+
canonicalHistory: provider?.nativeHistory,
|
|
2421
|
+
historySessionId,
|
|
2422
|
+
workspace,
|
|
2423
|
+
offset: 0,
|
|
2424
|
+
limit: historyLimit,
|
|
2425
|
+
excludeRecentCount: 0,
|
|
2426
|
+
historyBehavior: provider?.historyBehavior,
|
|
2427
|
+
scripts: provider?.scripts as any,
|
|
2428
|
+
});
|
|
2429
|
+
const lookup = (history as any)?.lookup === 'workspace' ? 'workspace' : 'session';
|
|
2430
|
+
const historyMessages = Array.isArray((history as any)?.messages)
|
|
2431
|
+
? normalizeAndFilterNativeHistory(h, agentStr, args, (history as any).messages as ChatMessage[], (history as any)?.providerSessionId)
|
|
2432
|
+
: [];
|
|
2433
|
+
const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
|
|
2434
|
+
? (history as any).providerSessionId
|
|
2435
|
+
: readHistorySessionIdFromMessages(historyMessages) || effectiveHistorySessionIdForRead;
|
|
2436
|
+
// Antigravity coordinator root fix (history-only path — the post-turn
|
|
2437
|
+
// read a coordinator actually hits: no live adapter, no pin, agy takes no
|
|
2438
|
+
// --session-id so spawnedAtMs is 0 after attach-restore → the read resolves
|
|
2439
|
+
// via the workspace-latest fallback, lookup === 'workspace'). The dispatcher
|
|
2440
|
+
// STILL surfaces the on-disk conversation uuid there, and flags whether it
|
|
2441
|
+
// was OWNER-token-confirmed as this session's own (an exact/birth pick) vs a
|
|
2442
|
+
// bare recency pick that could be a co-located replica's conversation.
|
|
2443
|
+
// • Pin the uuid on a workspace-latest read ONLY when owner-confirmed —
|
|
2444
|
+
// recording a replica's uuid would hard-wire the coordinator↔replica
|
|
2445
|
+
// crosswire permanently. Exact/session-scoped reads pin as before.
|
|
2446
|
+
// • Feed the owner-confirmed uuid as the explicit identity to the
|
|
2447
|
+
// safe-mapping check even on a workspace-latest read so the identity
|
|
2448
|
+
// test runs uuid-to-uuid and trusts the assistant on this FIRST read
|
|
2449
|
+
// (else it saw undefined identity → workspace-overlap branch → the PTY
|
|
2450
|
+
// snapshot has only the user echo → fail-closed → regress to pty-parser).
|
|
2451
|
+
const historyLookupIsWorkspace = lookup === 'workspace';
|
|
2452
|
+
const historyOwnerConfirmed = agentStr === 'antigravity-cli' && (history as any)?.ownerConfirmed === true;
|
|
2453
|
+
const historyOwnerConfirmedUuid = historyOwnerConfirmed
|
|
2454
|
+
&& typeof historyProviderSessionId === 'string' && historyProviderSessionId.trim()
|
|
2455
|
+
? historyProviderSessionId.trim()
|
|
2456
|
+
: '';
|
|
2457
|
+
// Refresh the pin whenever this path resolves a real provider id — but for
|
|
2458
|
+
// a workspace-latest antigravity read, only when the uuid is owner-confirmed.
|
|
2459
|
+
if (typeof (history as any)?.providerSessionId === 'string'
|
|
2460
|
+
&& (history as any).providerSessionId.trim()
|
|
2461
|
+
&& (!historyLookupIsWorkspace || !agentStr || agentStr !== 'antigravity-cli' || historyOwnerConfirmed)) {
|
|
2462
|
+
recordBoundProviderSessionId(h, effectiveReadSessionId(h, targetSid), (history as any).providerSessionId.trim());
|
|
2463
|
+
}
|
|
2464
|
+
// Use the id we actually read with (pin / real provider id), NOT the
|
|
2465
|
+
// raw runtime-fallback historySessionId — otherwise the mapping guard
|
|
2466
|
+
// compares the stamped messages' real id against the runtime id and
|
|
2467
|
+
// fails closed, undoing the pin reuse.
|
|
2468
|
+
const mappingSessionId = historyOwnerConfirmedUuid || effectiveHistorySessionIdForRead;
|
|
2469
|
+
// Fail closed for an antigravity workspace-latest read whose uuid was NOT
|
|
2470
|
+
// owner-confirmed: it is a bare recency/newest-by-mtime pick that could be
|
|
2471
|
+
// a co-located concurrent session's (replica's) conversation. Without an
|
|
2472
|
+
// owner-token confirmation we cannot prove ownership, so refuse it rather
|
|
2473
|
+
// than surface a sibling's transcript (the coordinator↔replica crosswire
|
|
2474
|
+
// guard). This is the same fail-closed default the design study protects —
|
|
2475
|
+
// only an owner-confirmed uuid escapes it above.
|
|
2476
|
+
const antigravityWorkspaceLatestUnconfirmed = agentStr === 'antigravity-cli'
|
|
2477
|
+
&& historyLookupIsWorkspace
|
|
2478
|
+
&& !historyOwnerConfirmedUuid;
|
|
2479
|
+
const safeMapping = supportsNative && !antigravityWorkspaceLatestUnconfirmed
|
|
2480
|
+
? hasSafeNativeHistoryMapping({
|
|
2481
|
+
historySessionId: historyOwnerConfirmedUuid || (lookup === 'workspace' ? undefined : mappingSessionId),
|
|
2482
|
+
providerSessionId: historyOwnerConfirmedUuid || (lookup === 'workspace' ? undefined : historyProviderSessionId),
|
|
2483
|
+
workspace,
|
|
2484
|
+
nativeMessages: historyMessages,
|
|
2485
|
+
})
|
|
2486
|
+
: false;
|
|
2487
|
+
const trustedExactNativeIdentity = (lookup !== 'workspace' || Boolean(historyOwnerConfirmedUuid))
|
|
2488
|
+
&& Boolean(mappingSessionId)
|
|
2489
|
+
&& Boolean(historyProviderSessionId)
|
|
2490
|
+
&& mappingSessionId === historyProviderSessionId;
|
|
2491
|
+
|
|
2492
|
+
const machineSessionKey = String(
|
|
2493
|
+
args?.targetSessionId
|
|
2494
|
+
|| historyProviderSessionId
|
|
2495
|
+
|| historySessionId
|
|
2496
|
+
|| (h.currentSession as any)?.sessionId
|
|
2497
|
+
|| ''
|
|
2498
|
+
);
|
|
2499
|
+
const decision = decideCliReadChatSource({
|
|
2500
|
+
providerType: agentStr,
|
|
2501
|
+
provider,
|
|
2502
|
+
sessionId: machineSessionKey,
|
|
2503
|
+
nativeHistoryResult: history,
|
|
2504
|
+
safeMapping,
|
|
2505
|
+
trustedExactNativeIdentity,
|
|
2506
|
+
sessionWorkspace: workspace,
|
|
2507
|
+
intendedWorkspace,
|
|
2508
|
+
ptyMessages: [],
|
|
2509
|
+
ptyStatusApprovalOnly: false,
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
if (supportsNative && !decision.nativeSelected) {
|
|
2513
|
+
// Native-only content preservation (hermes chat_tail gap).
|
|
2514
|
+
// The history-only path has NO PTY transcript (native-only
|
|
2515
|
+
// providers suppress PTY bodies), so args.ptyMessages is empty
|
|
2516
|
+
// and the machine's pty-parser selection returns NOTHING. But a
|
|
2517
|
+
// post-turn / cold read routinely lands here with a REAL,
|
|
2518
|
+
// safely-mapped native slice that the source FSM declined only
|
|
2519
|
+
// because coverage came back 'partial' (missing sessionStartedAtMs
|
|
2520
|
+
// → Booting→Recovering→pty-parser) or a transient shrink looked
|
|
2521
|
+
// like a regression. Dropping those rows deletes the assistant
|
|
2522
|
+
// answer from chat_tail / read_chat entirely. When the native
|
|
2523
|
+
// read actually resolved rows for THIS session identity
|
|
2524
|
+
// (safeMapping proves ownership: matching historySessionId /
|
|
2525
|
+
// providerSessionId + workspace), return them instead of an empty
|
|
2526
|
+
// array. This never loosens identity safety — it is gated on the
|
|
2527
|
+
// same hasSafeNativeHistoryMapping used everywhere else — and it
|
|
2528
|
+
// is scoped to the native-only history path (no PTY to prefer).
|
|
2529
|
+
// Truly-empty native reads (historyMessages.length === 0) and
|
|
2530
|
+
// unsafe/workspace-aliasing reads (safeMapping === false) still
|
|
2531
|
+
// fall through to the soft-pending dead-end below.
|
|
2532
|
+
if (safeMapping && historyMessages.length > 0) {
|
|
2533
|
+
LOG.debug('Command', `[read_chat] native-only content preserved despite pty-parser selection target=${String(args?.targetSessionId || '')} provider=${agentStr} rows=${historyMessages.length} cause=${decision.decision.transition.cause}`);
|
|
2534
|
+
return buildReadChatCommandResult({
|
|
2535
|
+
messages: historyMessages,
|
|
2536
|
+
status: 'idle',
|
|
2537
|
+
messageSource: {
|
|
2538
|
+
...decision.messageSource,
|
|
2539
|
+
nativeOnlyContentPreserved: true,
|
|
2540
|
+
returnedMessageCount: historyMessages.length,
|
|
2541
|
+
},
|
|
2542
|
+
transcriptProvenance: {
|
|
2543
|
+
...decision.messageSource,
|
|
2544
|
+
nativeOnlyContentPreserved: true,
|
|
2545
|
+
},
|
|
2546
|
+
...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
|
|
2547
|
+
...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
|
|
2548
|
+
...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')
|
|
2549
|
+
? { transcriptAuthority: (provider?.historyBehavior as any).transcriptAuthority }
|
|
2550
|
+
: {}),
|
|
2551
|
+
coverage: 'tail',
|
|
2552
|
+
}, args, h);
|
|
2553
|
+
}
|
|
2554
|
+
// Dead-end: we are in the history-only path (no live PTY/ACP
|
|
2555
|
+
// adapter was found for this target session) AND provider-native
|
|
2556
|
+
// history is not safely mappable to the requested session
|
|
2557
|
+
// (no historySessionId stamp / workspace mismatch). Previously
|
|
2558
|
+
// this returned `success:false`, which the command logger emits
|
|
2559
|
+
// at warn level on EVERY poll (handler.ts logCommandEnd) —
|
|
2560
|
+
// mesh coordinators poll read_chat continuously, so a worker whose
|
|
2561
|
+
// transcript can never be safely mapped produced a 100% warn-log
|
|
2562
|
+
// storm with no recovery. Switch to a SOFT response: success with
|
|
2563
|
+
// empty messages + pending:true so the coordinator treats it as
|
|
2564
|
+
// "no live messages readable yet" rather than a hard failure, and
|
|
2565
|
+
// carry the machine-readable reason for debuggability. The normal
|
|
2566
|
+
// live-adapter path (above) and the safe-native return (below) are
|
|
2567
|
+
// unaffected — this is strictly the both-absent dead end.
|
|
2568
|
+
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`);
|
|
2569
|
+
return {
|
|
2570
|
+
success: true,
|
|
2571
|
+
pending: true,
|
|
2572
|
+
// Both signals are true here: we reached the history-only path
|
|
2573
|
+
// because no live adapter was found (`live_adapter_not_found`),
|
|
2574
|
+
// and native history is not safely mappable
|
|
2575
|
+
// (`native_history_not_safely_available`).
|
|
2576
|
+
reason: 'native_history_not_safely_available',
|
|
2577
|
+
reasons: ['live_adapter_not_found', 'native_history_not_safely_available'],
|
|
2578
|
+
code: 'native_history_not_safely_available',
|
|
2579
|
+
messages: [],
|
|
2580
|
+
status: 'idle',
|
|
2581
|
+
providerSessionId: historyProviderSessionId,
|
|
2582
|
+
messageSource: decision.messageSource,
|
|
2583
|
+
transcriptProvenance: decision.messageSource,
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
return buildReadChatCommandResult({
|
|
2587
|
+
messages: historyMessages,
|
|
2588
|
+
status: 'idle',
|
|
2589
|
+
messageSource: decision.messageSource,
|
|
2590
|
+
transcriptProvenance: decision.messageSource,
|
|
2591
|
+
...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
|
|
2592
|
+
...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
|
|
2593
|
+
...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')
|
|
2594
|
+
? { transcriptAuthority: (provider?.historyBehavior as any).transcriptAuthority }
|
|
2595
|
+
: {}),
|
|
2596
|
+
coverage: 'tail',
|
|
2597
|
+
}, args, h);
|
|
2598
|
+
} catch (error: any) {
|
|
2599
|
+
return { success: false, error: error?.message || `${transport} adapter not found` };
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// Extension transport: evaluateInSession
|
|
2604
|
+
if (isExtensionTransport(transport)) {
|
|
2605
|
+
let extensionReadChatError = '';
|
|
2606
|
+
try {
|
|
2607
|
+
const evalResult = await h.evaluateProviderScript('readChat', undefined, READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS);
|
|
2608
|
+
if (evalResult?.result) {
|
|
2609
|
+
let parsed = evalResult.result;
|
|
2610
|
+
if (typeof parsed === 'string') {
|
|
2611
|
+
try {
|
|
2612
|
+
parsed = JSON.parse(parsed);
|
|
2613
|
+
} catch (e: any) {
|
|
2614
|
+
extensionReadChatError = `extension read_chat parse failed: ${e?.message || String(e)}`;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (parsed && typeof parsed === 'object') {
|
|
2618
|
+
const validated = validateReadChatResultPayload(parsed, 'extension read_chat');
|
|
2619
|
+
_log(`Extension OK: ${validated.messages?.length || 0} msgs`);
|
|
2620
|
+
traceProviderEvent(args, 'provider', 'extension.read_chat.success', {
|
|
2621
|
+
h,
|
|
2622
|
+
provider,
|
|
2623
|
+
payload: {
|
|
2624
|
+
method: 'evaluateProviderScript',
|
|
2625
|
+
result: evalResult.result,
|
|
2626
|
+
parsed: validated,
|
|
2627
|
+
messageCount: Array.isArray(validated.messages) ? validated.messages.length : 0,
|
|
2628
|
+
},
|
|
2629
|
+
});
|
|
2630
|
+
h.historyWriter.appendNewMessages(
|
|
2631
|
+
provider?.type || 'unknown_extension',
|
|
2632
|
+
toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
|
|
2633
|
+
validated.title,
|
|
2634
|
+
args?.targetSessionId,
|
|
2635
|
+
historySessionId,
|
|
2636
|
+
);
|
|
2637
|
+
return buildReadChatCommandResult(validated as Record<string, any>, args, h);
|
|
2638
|
+
}
|
|
2639
|
+
if (!extensionReadChatError) {
|
|
2640
|
+
extensionReadChatError = 'extension read_chat returned a non-object payload';
|
|
2641
|
+
}
|
|
2642
|
+
} else {
|
|
2643
|
+
extensionReadChatError = 'extension read_chat returned no payload';
|
|
2644
|
+
}
|
|
2645
|
+
} catch (e: any) {
|
|
2646
|
+
extensionReadChatError = `extension read_chat failed: ${e?.message || String(e)}`;
|
|
2647
|
+
_log(`Extension error: ${e.message}`);
|
|
2648
|
+
traceProviderEvent(args, 'provider', 'extension.read_chat.error', {
|
|
2649
|
+
h,
|
|
2650
|
+
provider,
|
|
2651
|
+
level: 'warn',
|
|
2652
|
+
payload: { method: 'evaluateProviderScript', error: e.message },
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
// Alternative: AgentStreamManager (script fail when)
|
|
2656
|
+
if (h.agentStream) {
|
|
2657
|
+
const cdp = h.getCdp();
|
|
2658
|
+
const parentSessionId = h.currentSession?.parentSessionId;
|
|
2659
|
+
if (cdp && parentSessionId) {
|
|
2660
|
+
const stream = await h.agentStream.collectActiveSession(cdp, parentSessionId);
|
|
2661
|
+
if (stream && stream.agentType !== provider?.type) {
|
|
2662
|
+
return { success: false, error: `extension read_chat stream agent mismatch for ${provider?.type || 'unknown_extension'}` };
|
|
2663
|
+
}
|
|
2664
|
+
if (stream) {
|
|
2665
|
+
h.historyWriter.appendNewMessages(
|
|
2666
|
+
stream.agentType,
|
|
2667
|
+
toHistoryPersistedMessages(stream.messages || []),
|
|
2668
|
+
undefined,
|
|
2669
|
+
args?.targetSessionId,
|
|
2670
|
+
historySessionId,
|
|
2671
|
+
);
|
|
2672
|
+
return buildReadChatCommandResult({
|
|
2673
|
+
messages: stream.messages || [],
|
|
2674
|
+
status: stream.status,
|
|
2675
|
+
agentType: stream.agentType,
|
|
2676
|
+
}, args, h);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return { success: false, error: extensionReadChatError || 'extension read_chat unavailable' };
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// IDE category (default): cdp.evaluate
|
|
2684
|
+
const cdp = h.getCdp();
|
|
2685
|
+
if (!cdp?.isConnected) return { success: false, error: 'CDP not connected' };
|
|
2686
|
+
|
|
2687
|
+
// webview IDE (Kiro, PearAI) → evaluateInWebviewFrame directly use
|
|
2688
|
+
const webviewScript = h.getProviderScript('webviewReadChat') || h.getProviderScript('webview_read_chat');
|
|
2689
|
+
if (webviewScript) {
|
|
2690
|
+
let webviewReadChatError = '';
|
|
2691
|
+
try {
|
|
2692
|
+
const matchText = provider?.webviewMatchText;
|
|
2693
|
+
const matchFn = matchText
|
|
2694
|
+
? (body: string) => body.includes(matchText)
|
|
2695
|
+
: undefined;
|
|
2696
|
+
const raw = await cdp.evaluateInWebviewFrame(webviewScript, matchFn);
|
|
2697
|
+
if (raw) {
|
|
2698
|
+
let parsed: any = raw;
|
|
2699
|
+
if (typeof parsed === 'string') {
|
|
2700
|
+
try {
|
|
2701
|
+
parsed = JSON.parse(parsed);
|
|
2702
|
+
} catch (e: any) {
|
|
2703
|
+
webviewReadChatError = `webview read_chat parse failed: ${e?.message || String(e)}`;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
if (parsed && typeof parsed === 'object') {
|
|
2707
|
+
const validated = validateReadChatResultPayload(parsed, 'webview read_chat');
|
|
2708
|
+
_log(`Webview OK: ${validated.messages?.length || 0} msgs`);
|
|
2709
|
+
h.historyWriter.appendNewMessages(
|
|
2710
|
+
provider?.type || getCurrentProviderType(h, 'unknown_webview'),
|
|
2711
|
+
toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
|
|
2712
|
+
validated.title,
|
|
2713
|
+
args?.targetSessionId,
|
|
2714
|
+
historySessionId,
|
|
2715
|
+
);
|
|
2716
|
+
return buildReadChatCommandResult(validated as Record<string, any>, args, h);
|
|
2717
|
+
}
|
|
2718
|
+
if (!webviewReadChatError) {
|
|
2719
|
+
webviewReadChatError = 'webview read_chat returned a non-object payload';
|
|
2720
|
+
}
|
|
2721
|
+
} else {
|
|
2722
|
+
webviewReadChatError = 'webview read_chat returned no payload';
|
|
2723
|
+
}
|
|
2724
|
+
} catch (e: any) {
|
|
2725
|
+
webviewReadChatError = `webview read_chat failed: ${e?.message || String(e)}`;
|
|
2726
|
+
_log(`Webview readChat error: ${e.message}`);
|
|
2727
|
+
}
|
|
2728
|
+
return { success: false, error: webviewReadChatError || 'webview read_chat unavailable' };
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Regular IDE (Cursor, Windsurf, Trae etc) → main DOM evaluate
|
|
2732
|
+
const script = h.getProviderScript('readChat') || h.getProviderScript('read_chat');
|
|
2733
|
+
if (script) {
|
|
2734
|
+
let ideReadChatError = '';
|
|
2735
|
+
try {
|
|
2736
|
+
const evalResult = await h.evaluateProviderScript('readChat', undefined, READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS);
|
|
2737
|
+
if (evalResult?.result) {
|
|
2738
|
+
let parsed: any = evalResult.result;
|
|
2739
|
+
if (typeof parsed === 'string') {
|
|
2740
|
+
try {
|
|
2741
|
+
parsed = JSON.parse(parsed);
|
|
2742
|
+
} catch (e: any) {
|
|
2743
|
+
ideReadChatError = `ide read_chat parse failed: ${e?.message || String(e)}`;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (parsed && typeof parsed === 'object') {
|
|
2747
|
+
const validated = validateReadChatResultPayload(parsed, 'ide read_chat');
|
|
2748
|
+
_log(`OK: ${validated.messages?.length || 0} msgs`);
|
|
2749
|
+
traceProviderEvent(args, 'provider', 'ide.read_chat.success', {
|
|
2750
|
+
h,
|
|
2751
|
+
provider,
|
|
2752
|
+
payload: {
|
|
2753
|
+
method: 'evaluate',
|
|
2754
|
+
result: evalResult.result,
|
|
2755
|
+
parsed: validated,
|
|
2756
|
+
messageCount: Array.isArray(validated.messages) ? validated.messages.length : 0,
|
|
2757
|
+
},
|
|
2758
|
+
});
|
|
2759
|
+
h.historyWriter.appendNewMessages(
|
|
2760
|
+
provider?.type || getCurrentProviderType(h, 'unknown_ide'),
|
|
2761
|
+
toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
|
|
2762
|
+
validated.title,
|
|
2763
|
+
args?.targetSessionId,
|
|
2764
|
+
historySessionId,
|
|
2765
|
+
);
|
|
2766
|
+
return buildReadChatCommandResult(validated as Record<string, any>, args, h);
|
|
2767
|
+
}
|
|
2768
|
+
if (!ideReadChatError) {
|
|
2769
|
+
ideReadChatError = 'ide read_chat returned a non-object payload';
|
|
2770
|
+
}
|
|
2771
|
+
} else {
|
|
2772
|
+
ideReadChatError = 'ide read_chat returned no payload';
|
|
2773
|
+
}
|
|
2774
|
+
} catch (e: any) {
|
|
2775
|
+
ideReadChatError = `ide read_chat failed: ${e?.message || String(e)}`;
|
|
2776
|
+
LOG.info('Command', `[read_chat] Script error: ${e.message}`);
|
|
2777
|
+
traceProviderEvent(args, 'provider', 'ide.read_chat.error', {
|
|
2778
|
+
h,
|
|
2779
|
+
provider,
|
|
2780
|
+
level: 'warn',
|
|
2781
|
+
payload: { method: 'evaluate', error: e.message },
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
return { success: false, error: ideReadChatError || 'ide read_chat unavailable' };
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
return { success: false, error: 'read_chat unavailable' };
|
|
2788
|
+
}
|