@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.491
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 +66 -0
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/registry-resolver.d.ts +54 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/config/state-store.d.ts +30 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/cli-detector.d.ts +17 -0
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +26 -2
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +80 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/git-worktree.d.ts +71 -1
- package/dist/git/index.d.ts +3 -1
- package/dist/index.d.ts +70 -17
- package/dist/index.js +53556 -19074
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +53380 -19063
- 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 +1309 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +167 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +153 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router-aggregate-status.ts +209 -0
- package/src/commands/router-mesh-session-owner.ts +114 -0
- package/src/commands/router-refine.ts +1794 -0
- package/src/commands/router-worktree-cleanup.ts +870 -0
- package/src/commands/router.ts +567 -4149
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +517 -24
- package/src/config/config.ts +30 -0
- package/src/config/mesh-config.ts +300 -24
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/registry-resolver.ts +100 -0
- package/src/config/repo-settings.ts +111 -0
- package/src/config/state-store.ts +55 -0
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +94 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +112 -16
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +822 -48
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +261 -4
- package/src/git/index.ts +17 -0
- package/src/index.ts +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,1241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative native-history executor.
|
|
3
|
+
*
|
|
4
|
+
* Reads a spec.json's `native_history.source` block and turns it into a
|
|
5
|
+
* NativeHistoryResult by talking directly to the on-disk store (jsonl
|
|
6
|
+
* file or sqlite db). No per-provider TypeScript reader required —
|
|
7
|
+
* adding a new provider is just authoring spec.json.
|
|
8
|
+
*
|
|
9
|
+
* Two source kinds:
|
|
10
|
+
* - jsonl — newest matching file inside a path (with variable expansion)
|
|
11
|
+
* + jsonpath-lite map for each record
|
|
12
|
+
* - sqlite — read-only better-sqlite3 handle, two queries (session pick
|
|
13
|
+
* + message fetch) + jsonpath-lite map for each row
|
|
14
|
+
*
|
|
15
|
+
* Exotic formats can still ship a provider-local reader via
|
|
16
|
+
* `native_history.override_path` — the dispatcher in provider-loader picks
|
|
17
|
+
* that path instead of calling this executor.
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as os from 'node:os';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import { LOG } from '../../logging/logger.js';
|
|
25
|
+
import { loadBetterSqlite3 } from '../../system/load-better-sqlite3.js';
|
|
26
|
+
import type {
|
|
27
|
+
NativeHistoryConfig,
|
|
28
|
+
NativeHistoryJsonlSource,
|
|
29
|
+
NativeHistoryMessageMap,
|
|
30
|
+
NativeHistorySqliteSource,
|
|
31
|
+
NativeHistoryToolMap,
|
|
32
|
+
} from './types.js';
|
|
33
|
+
import { SPAWN_BIND_GRACE_MS } from '../native-history/constants.js';
|
|
34
|
+
|
|
35
|
+
export interface NativeHistoryInput {
|
|
36
|
+
agentType?: string;
|
|
37
|
+
sessionId?: string;
|
|
38
|
+
providerSessionId?: string;
|
|
39
|
+
historySessionId?: string;
|
|
40
|
+
workspace?: string;
|
|
41
|
+
/** Daemon-side wall clock at the moment the session was registered.
|
|
42
|
+
* Native-history file lookups use this as the lower bound: any file
|
|
43
|
+
* whose mtime is before the current session started can't be from
|
|
44
|
+
* this session, so it's excluded from newest-recent matching. The
|
|
45
|
+
* caller (chat-history pipeline) populates this from the session
|
|
46
|
+
* registry; specs/executor never need to know how it's sourced. */
|
|
47
|
+
sessionStartedAtMs?: number;
|
|
48
|
+
/** Env overrides the daemon set on the spawned CLI. The mesh
|
|
49
|
+
* coordinator points hermes at a per-coordinator HERMES_HOME so
|
|
50
|
+
* the hermes process writes its state.db into a tmp directory
|
|
51
|
+
* instead of ~/.hermes. expandPath consults this map before
|
|
52
|
+
* process.env so the native-history reader follows the spawned
|
|
53
|
+
* child's view of HERMES_HOME / similar overrides; without it
|
|
54
|
+
* the reader would always look at ~/.hermes and miss every
|
|
55
|
+
* coordinator-session transcript. */
|
|
56
|
+
envOverrides?: Record<string, string>;
|
|
57
|
+
args?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface NativeHistoryMessage {
|
|
61
|
+
role: 'user' | 'assistant' | 'system';
|
|
62
|
+
content: string;
|
|
63
|
+
receivedAt: number;
|
|
64
|
+
kind?: string;
|
|
65
|
+
workspace?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface NativeHistoryResult {
|
|
69
|
+
messages: NativeHistoryMessage[];
|
|
70
|
+
providerSessionId?: string;
|
|
71
|
+
sourcePath: string;
|
|
72
|
+
sourceMtimeMs: number;
|
|
73
|
+
nativeHistoryCoverage?: 'full' | 'partial' | 'best-effort';
|
|
74
|
+
workspace?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
|
|
78
|
+
|
|
79
|
+
export function executeNativeHistory(cfg: NativeHistoryConfig, input: NativeHistoryInput): NativeHistoryResult | null {
|
|
80
|
+
if (!cfg?.source) return null;
|
|
81
|
+
if (cfg.source.kind === 'jsonl') return executeJsonl(cfg.source, input);
|
|
82
|
+
if (cfg.source.kind === 'sqlite') return executeSqlite(cfg.source, input);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
// JSONL
|
|
88
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput): NativeHistoryResult | null {
|
|
91
|
+
const resolved = expandPath(src.path, input);
|
|
92
|
+
if (!resolved) return null;
|
|
93
|
+
|
|
94
|
+
const windowMs = typeof src.recent_window_ms === 'number' ? src.recent_window_ms : 5 * 60_000;
|
|
95
|
+
const filePat = src.file_pattern ? globToRegex(src.file_pattern) : /.*\.jsonl$/;
|
|
96
|
+
const requestedSessionId = readRequestedSessionId(input);
|
|
97
|
+
|
|
98
|
+
// path can be:
|
|
99
|
+
// - a concrete file → used as-is
|
|
100
|
+
// - a concrete dir → newest matching file inside recent_window_ms
|
|
101
|
+
// - a path with `*` or `**` segments → walk all dirs that match the
|
|
102
|
+
// glob, pick the newest matching file across all matches. Lets
|
|
103
|
+
// specs like ~/.gemini/antigravity-cli/brain/*/.system_generated/logs
|
|
104
|
+
// resolve transparently without a per-provider override.
|
|
105
|
+
// The session-start cutoff guarantees a fresh dashboard view can't
|
|
106
|
+
// pick up a transcript file from a session that ended before this
|
|
107
|
+
// one started. recent_window_ms only controls how far back we'd
|
|
108
|
+
// otherwise look for a matching file; the session-start floor wins
|
|
109
|
+
// when it's later.
|
|
110
|
+
const sessionFloor = typeof input.sessionStartedAtMs === 'number' ? input.sessionStartedAtMs : 0;
|
|
111
|
+
// When multiple concurrent CLI sessions in the same workspace each create
|
|
112
|
+
// their own rollout (e.g. two codex-cli sessions both writing into
|
|
113
|
+
// ~/.codex/sessions/{date}), `newestRecentFile` picks the same file for
|
|
114
|
+
// every reader and the daemon sessions cross-alias each other. Prefer
|
|
115
|
+
// session-meta-aware matching: the candidate whose meta.cwd matches the
|
|
116
|
+
// workspace AND whose meta.timestamp is closest to (or within
|
|
117
|
+
// spawnGraceMs of) the daemon's spawn time wins. Falls back to mtime
|
|
118
|
+
// ordering when no candidate exposes a usable session_meta.
|
|
119
|
+
const workspaceHint = typeof input.workspace === 'string' && input.workspace.trim() ? input.workspace.trim() : '';
|
|
120
|
+
let sourcePath: string | null = null;
|
|
121
|
+
if (resolved.includes('*')) {
|
|
122
|
+
sourcePath = pickExactSessionFileAcrossGlob(resolved, filePat, requestedSessionId)
|
|
123
|
+
|| pickSessionBoundFileAcrossGlob(resolved, filePat, windowMs, sessionFloor, workspaceHint)
|
|
124
|
+
|| newestRecentFileAcrossGlob(resolved, filePat, windowMs, sessionFloor);
|
|
125
|
+
} else {
|
|
126
|
+
let stat: fs.Stats | null = null;
|
|
127
|
+
try { stat = fs.statSync(resolved); } catch { /* fall through to date-walk fallback */ }
|
|
128
|
+
if (stat && stat.isFile()) {
|
|
129
|
+
sourcePath = resolved;
|
|
130
|
+
} else if (stat && stat.isDirectory()) {
|
|
131
|
+
sourcePath = pickExactSessionFile(resolved, filePat, requestedSessionId)
|
|
132
|
+
|| (requestedSessionId ? null : pickSessionBoundFile(resolved, filePat, windowMs, sessionFloor, workspaceHint))
|
|
133
|
+
|| (requestedSessionId ? null : newestRecentFile(resolved, filePat, windowMs, sessionFloor));
|
|
134
|
+
}
|
|
135
|
+
// Date-templated directories (e.g. ~/.codex/sessions/{yyyy}/{mm}/{dd})
|
|
136
|
+
// can drift from the provider's chosen calendar day because CLIs
|
|
137
|
+
// disagree on local-vs-UTC date buckets. Search nearby date dirs
|
|
138
|
+
// before falling back to non-exact matching.
|
|
139
|
+
if (!sourcePath && hasDateTemplateSegment(src.path)) {
|
|
140
|
+
sourcePath = pickExactSessionFileAcrossDateWindow(src.path, input, filePat, requestedSessionId)
|
|
141
|
+
|| (requestedSessionId ? null : pickSessionBoundFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor, workspaceHint))
|
|
142
|
+
|| (requestedSessionId ? null : newestRecentFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor));
|
|
143
|
+
}
|
|
144
|
+
// Raw-workspace slug candidate: `resolved` derives its {cwd*} slug from
|
|
145
|
+
// fs.realpathSync(workspace). On Windows realpath normalizes the path
|
|
146
|
+
// (drive-letter case D:↔d:, \\?\ long-path prefix, junction expansion)
|
|
147
|
+
// so the slug can diverge from the one the CLI actually wrote — and the
|
|
148
|
+
// concrete path above then misses with ENOENT. Retry with the slug built
|
|
149
|
+
// from the RAW workspace string before falling back to a scan; it's the
|
|
150
|
+
// cheap fix when realpath divergence is the only problem.
|
|
151
|
+
if (!sourcePath && !hasDateTemplateSegment(src.path)) {
|
|
152
|
+
const resolvedRaw = expandPath(src.path, input, { skipWorkspaceRealpath: true });
|
|
153
|
+
if (resolvedRaw && resolvedRaw !== resolved) {
|
|
154
|
+
try {
|
|
155
|
+
const rawStat = fs.statSync(resolvedRaw);
|
|
156
|
+
if (rawStat.isFile()) sourcePath = resolvedRaw;
|
|
157
|
+
else if (rawStat.isDirectory()) {
|
|
158
|
+
sourcePath = pickExactSessionFile(resolvedRaw, filePat, requestedSessionId)
|
|
159
|
+
|| (requestedSessionId ? null : newestRecentFile(resolvedRaw, filePat, windowMs, sessionFloor));
|
|
160
|
+
}
|
|
161
|
+
} catch { /* raw slug also missed — fall through to scan */ }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Last-resort scan: the slug-derived directory missed entirely (the
|
|
165
|
+
// dominant Windows failure: realpath/raw slug both diverge from the CLI's
|
|
166
|
+
// on-disk project dir → 0 messages, no PTY fallback for native-source
|
|
167
|
+
// providers). When we have an exact session id, walk the projects root
|
|
168
|
+
// for `<sessionId>.jsonl` regardless of which project subdir holds it.
|
|
169
|
+
// The session id is a UUID, so basename matching is unambiguous; mirrors
|
|
170
|
+
// the standalone reader's scan (claude-cli-transcript.ts resolveTranscriptPath).
|
|
171
|
+
if (!sourcePath && requestedSessionId) {
|
|
172
|
+
sourcePath = scanProjectsRootForSessionFile(src.path, input, requestedSessionId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!sourcePath) {
|
|
176
|
+
// Was silent before — a slug miss produced 0 messages with no trace, so
|
|
177
|
+
// a live read_chat returning empty was indistinguishable from "no file"
|
|
178
|
+
// vs "wrong path". Log the attempted concrete path + both slug variants
|
|
179
|
+
// so the failure mode is greppable in daemon logs.
|
|
180
|
+
const wsRaw = typeof input.workspace === 'string' ? input.workspace : '';
|
|
181
|
+
let wsReal = wsRaw;
|
|
182
|
+
try { if (wsRaw) wsReal = fs.realpathSync(wsRaw); } catch { /* keep raw */ }
|
|
183
|
+
LOG.debug('NativeHistory', `jsonl unresolved: tried=${JSON.stringify(resolved)} sessionId=${requestedSessionId || '(none)'} wsRaw=${JSON.stringify(wsRaw)} wsReal=${JSON.stringify(wsReal)} rawSlug=${JSON.stringify(claudeProjectDirName(wsRaw))} realSlug=${JSON.stringify(claudeProjectDirName(wsReal))} (concrete miss + raw-slug retry + projects scan all failed)`);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const mtime = safeMtimeMs(sourcePath);
|
|
188
|
+
const lines = readJsonlLines(sourcePath);
|
|
189
|
+
if (lines.length === 0) return null;
|
|
190
|
+
const transcriptWorkspace = readSessionMetaWorkspace(lines);
|
|
191
|
+
|
|
192
|
+
// session id: filename uuid or extracted from first record
|
|
193
|
+
let providerSessionId: string | undefined;
|
|
194
|
+
if (src.session_id_from === 'first_record' && src.session_id_path) {
|
|
195
|
+
const v = jsonPathGet(lines[0], src.session_id_path);
|
|
196
|
+
if (typeof v === 'string' && v) providerSessionId = v;
|
|
197
|
+
} else if (src.session_id_from === 'filename_uuid' || !src.session_id_from) {
|
|
198
|
+
const m = path.basename(sourcePath).match(UUID_RE);
|
|
199
|
+
if (m) providerSessionId = m[1];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const requested = requestedSessionId || '';
|
|
203
|
+
if (requested && providerSessionId && providerSessionId !== requested) return null;
|
|
204
|
+
|
|
205
|
+
const filter = src.message_filter ? compileWhere(src.message_filter.where) : null;
|
|
206
|
+
const messages: NativeHistoryMessage[] = [];
|
|
207
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
208
|
+
const rec = lines[i];
|
|
209
|
+
if (filter && !filter(rec)) continue;
|
|
210
|
+
for (const msg of projectMessages(rec, src.message_map, i, lines.length, mtime)) {
|
|
211
|
+
if (transcriptWorkspace) msg.workspace = transcriptWorkspace;
|
|
212
|
+
messages.push(msg);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (messages.length === 0) return null;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
messages,
|
|
219
|
+
providerSessionId,
|
|
220
|
+
sourcePath,
|
|
221
|
+
sourceMtimeMs: mtime,
|
|
222
|
+
nativeHistoryCoverage: 'full',
|
|
223
|
+
workspace: transcriptWorkspace,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function readSessionMetaWorkspace(lines: any[]): string | undefined {
|
|
228
|
+
for (const record of lines.slice(0, 5)) {
|
|
229
|
+
if (String(record?.type ?? '') !== 'session_meta') continue;
|
|
230
|
+
const cwd = typeof record?.payload?.cwd === 'string' ? record.payload.cwd.trim() : '';
|
|
231
|
+
if (cwd) return cwd;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readJsonlLines(p: string): any[] {
|
|
237
|
+
let text: string;
|
|
238
|
+
try { text = fs.readFileSync(p, 'utf8'); } catch { return []; }
|
|
239
|
+
const out: any[] = [];
|
|
240
|
+
for (const line of text.split('\n')) {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (!trimmed) continue;
|
|
243
|
+
try { out.push(JSON.parse(trimmed)); } catch { /* skip malformed line */ }
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// SQLite
|
|
250
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput): NativeHistoryResult | null {
|
|
253
|
+
const resolved = expandPath(src.path, input);
|
|
254
|
+
if (!resolved || !fs.existsSync(resolved)) return null;
|
|
255
|
+
|
|
256
|
+
let Database: any;
|
|
257
|
+
try {
|
|
258
|
+
Database = loadBetterSqlite3();
|
|
259
|
+
} catch { return null; }
|
|
260
|
+
|
|
261
|
+
let db: any;
|
|
262
|
+
try { db = new Database(resolved, { readonly: true, fileMustExist: true }); }
|
|
263
|
+
catch { return null; }
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const requested = input.providerSessionId || '';
|
|
267
|
+
// Resolve the session id the message query runs against. The `requested`
|
|
268
|
+
// pin path is tried first, but a pinned id that has NO rows in the store
|
|
269
|
+
// is not a real session — fall back to the newest-session `session_query`
|
|
270
|
+
// instead of returning empty. This is the hermes read_chat gap: hermes
|
|
271
|
+
// never surfaces its own provider session id to the daemon (the spec
|
|
272
|
+
// declares no session-id extraction and the adapter's screen-scrape is
|
|
273
|
+
// codex-only), so the read pipeline falls back to threading the mesh
|
|
274
|
+
// RUNTIME session id through as `providerSessionId`. That runtime id does
|
|
275
|
+
// not exist in ~/.hermes/state.db, so the old unconditional pin path ran
|
|
276
|
+
// `message_query WHERE session_id = '<runtime id>'` → 0 rows → null, and
|
|
277
|
+
// the answer (physically present under the real cli session) was never
|
|
278
|
+
// returned. Validating the pin by the spec's own `message_query` keeps
|
|
279
|
+
// this schema-agnostic and only rescues the mis-bound-id case: a genuine
|
|
280
|
+
// discovered pin (codex/claude use jsonl sources and never reach here;
|
|
281
|
+
// any real sqlite pin has rows) still short-circuits on its own rows.
|
|
282
|
+
// Expand an anchor session id to every session id in its logical
|
|
283
|
+
// cluster. When the spec declares `session_cluster_query` the anchor is
|
|
284
|
+
// run through it (bound `?`) and each returned row's FIRST column is a
|
|
285
|
+
// cluster member id — typically a WITH RECURSIVE walk up to the cluster
|
|
286
|
+
// root and back down through all descendants, so passing a root, middle,
|
|
287
|
+
// or leaf anchor all resolve the same complete set. The anchor is always
|
|
288
|
+
// included even if the query omits it (defensive) so a spec with no
|
|
289
|
+
// cluster query, or a query that returns nothing, still reads the anchor
|
|
290
|
+
// itself. Absent query → just the anchor (single-session behaviour).
|
|
291
|
+
const resolveClusterIds = (anchorId: string): string[] => {
|
|
292
|
+
const ids = new Set<string>();
|
|
293
|
+
if (anchorId) ids.add(anchorId);
|
|
294
|
+
if (src.session_cluster_query && anchorId) {
|
|
295
|
+
try {
|
|
296
|
+
const rows: any[] = db.prepare(src.session_cluster_query).all(anchorId);
|
|
297
|
+
for (const row of rows) {
|
|
298
|
+
const idRaw = Object.values(row)[0];
|
|
299
|
+
if (idRaw != null && String(idRaw)) ids.add(String(idRaw));
|
|
300
|
+
}
|
|
301
|
+
} catch { /* fall back to anchor-only on a malformed cluster query */ }
|
|
302
|
+
}
|
|
303
|
+
return Array.from(ids);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Read messages for an anchor's WHOLE cluster, merged and re-sorted by
|
|
307
|
+
// their mapped timestamp so bubbles from different sub-sessions interleave
|
|
308
|
+
// in true chronological order (the turn's final assistant — written into a
|
|
309
|
+
// descendant sub-session in the split-turn case — lands last). No per-session
|
|
310
|
+
// short-circuit: an anchor whose OWN row has zero messages (hermes writes a
|
|
311
|
+
// 0-message intermediate `sessions` row) still yields the cluster's rows,
|
|
312
|
+
// and the whole cluster is scanned rather than stopping at the first
|
|
313
|
+
// non-empty session. Returns null only when the ENTIRE cluster is empty,
|
|
314
|
+
// preserving the pin-validation contract below (a pin that resolves no rows
|
|
315
|
+
// anywhere is a mis-bound id and falls through to newest-session recovery).
|
|
316
|
+
const resolveMessagesFor = (anchorId: string): any[] | null => {
|
|
317
|
+
if (!anchorId) return null;
|
|
318
|
+
const clusterIds = resolveClusterIds(anchorId);
|
|
319
|
+
const merged: any[] = [];
|
|
320
|
+
for (const id of clusterIds) {
|
|
321
|
+
let rows: any[];
|
|
322
|
+
try { rows = db.prepare(src.message_query).all(id); }
|
|
323
|
+
catch { continue; }
|
|
324
|
+
if (rows && rows.length > 0) merged.push(...rows);
|
|
325
|
+
}
|
|
326
|
+
if (merged.length === 0) return null;
|
|
327
|
+
if (clusterIds.length > 1) sortRowsByMappedTimestamp(merged, src.message_map);
|
|
328
|
+
return merged;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const resolveNewestSessionId = (): string => {
|
|
332
|
+
let sessionRow: any;
|
|
333
|
+
try {
|
|
334
|
+
// session_query may reference `?` to receive the session's
|
|
335
|
+
// start-time floor in seconds (e.g. WHERE started_at >= ?).
|
|
336
|
+
// That gives spec authors a robust way to keep prior-session
|
|
337
|
+
// rows out of a fresh dashboard view without inventing their
|
|
338
|
+
// own time arithmetic in SQL. When the caller didn't pass a
|
|
339
|
+
// session floor (i.e. no live session is associated with the
|
|
340
|
+
// call), we use 0 so spec queries that bind `?` still produce
|
|
341
|
+
// a sane result rather than choking the whole executor.
|
|
342
|
+
const sessionFloorSeconds = typeof input.sessionStartedAtMs === 'number'
|
|
343
|
+
? Math.floor(input.sessionStartedAtMs / 1000)
|
|
344
|
+
: 0;
|
|
345
|
+
const stmt = db.prepare(src.session_query);
|
|
346
|
+
try {
|
|
347
|
+
sessionRow = stmt.get(sessionFloorSeconds);
|
|
348
|
+
} catch {
|
|
349
|
+
sessionRow = stmt.get();
|
|
350
|
+
}
|
|
351
|
+
} catch { return ''; }
|
|
352
|
+
if (!sessionRow) return '';
|
|
353
|
+
// First column of the first row is the session id.
|
|
354
|
+
const sessionIdRaw = Object.values(sessionRow)[0];
|
|
355
|
+
return sessionIdRaw == null ? '' : String(sessionIdRaw);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
let sessionId: string;
|
|
359
|
+
let messageRows: any[] | null;
|
|
360
|
+
if (requested) {
|
|
361
|
+
// Pin path: read the requested session directly and skip the
|
|
362
|
+
// newest-wins `session_query`. hermes ≥0.14 spawns a fresh
|
|
363
|
+
// `sessions` row per internal sub-session, so an unpinned
|
|
364
|
+
// `ORDER BY started_at DESC LIMIT 1` pick drifts to a different id
|
|
365
|
+
// on every read (re-bind churn + reading completion evidence from
|
|
366
|
+
// the wrong session). A pin that resolves rows is authoritative.
|
|
367
|
+
messageRows = resolveMessagesFor(requested);
|
|
368
|
+
if (messageRows) {
|
|
369
|
+
sessionId = requested;
|
|
370
|
+
} else {
|
|
371
|
+
// The pinned id has no rows — it is not a real session in this
|
|
372
|
+
// store (the mis-bound mesh runtime-id case). Recover by letting
|
|
373
|
+
// the spec's own newest-session query self-resolve instead of
|
|
374
|
+
// returning empty.
|
|
375
|
+
sessionId = resolveNewestSessionId();
|
|
376
|
+
messageRows = resolveMessagesFor(sessionId);
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
sessionId = resolveNewestSessionId();
|
|
380
|
+
messageRows = resolveMessagesFor(sessionId);
|
|
381
|
+
}
|
|
382
|
+
if (!sessionId) return null;
|
|
383
|
+
if (!messageRows || messageRows.length === 0) return null;
|
|
384
|
+
|
|
385
|
+
const mtime = safeMtimeMs(resolved);
|
|
386
|
+
const messages: NativeHistoryMessage[] = [];
|
|
387
|
+
for (let i = 0; i < messageRows.length; i += 1) {
|
|
388
|
+
for (const msg of projectMessages(messageRows[i], src.message_map, i, messageRows.length, mtime)) {
|
|
389
|
+
messages.push(msg);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (messages.length === 0) return null;
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
messages,
|
|
396
|
+
providerSessionId: sessionId,
|
|
397
|
+
sourcePath: resolved,
|
|
398
|
+
sourceMtimeMs: mtime,
|
|
399
|
+
nativeHistoryCoverage: 'full',
|
|
400
|
+
};
|
|
401
|
+
} finally {
|
|
402
|
+
try { db.close(); } catch { /* ignore */ }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Stable-sort merged cluster rows by their mapped timestamp so bubbles read
|
|
408
|
+
* from different sub-sessions interleave in true chronological order. Uses the
|
|
409
|
+
* same `message_map.timestamp_ms` jsonpath + `parseTimestamp` heuristic the
|
|
410
|
+
* projection uses, so the sort key agrees with the receivedAt each row will be
|
|
411
|
+
* given. Rows with no resolvable timestamp keep their pre-sort relative order
|
|
412
|
+
* (stable), and equal timestamps preserve insertion order — both matter because
|
|
413
|
+
* a turn's terminal bubbles can share a sub-second timestamp.
|
|
414
|
+
*/
|
|
415
|
+
function sortRowsByMappedTimestamp(rows: any[], map: NativeHistoryMessageMap): void {
|
|
416
|
+
if (!map.timestamp_ms) return;
|
|
417
|
+
const keyed = rows.map((row, index) => {
|
|
418
|
+
const parsed = parseTimestamp(jsonPathGet(row, map.timestamp_ms as string));
|
|
419
|
+
return { row, index, ts: parsed == null ? Number.NaN : parsed };
|
|
420
|
+
});
|
|
421
|
+
keyed.sort((a, b) => {
|
|
422
|
+
const aHas = !Number.isNaN(a.ts);
|
|
423
|
+
const bHas = !Number.isNaN(b.ts);
|
|
424
|
+
if (aHas && bHas && a.ts !== b.ts) return a.ts - b.ts;
|
|
425
|
+
// Missing-timestamp rows and ties fall back to original insertion order
|
|
426
|
+
// so the sort stays stable.
|
|
427
|
+
return a.index - b.index;
|
|
428
|
+
});
|
|
429
|
+
for (let i = 0; i < keyed.length; i += 1) rows[i] = keyed[i].row;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
433
|
+
// Path expansion + globbing
|
|
434
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
function expandPath(template: string, input: NativeHistoryInput, opts?: { skipWorkspaceRealpath?: boolean }): string | null {
|
|
437
|
+
if (!template) return null;
|
|
438
|
+
let out = template;
|
|
439
|
+
if (out.startsWith('~/') || out === '~') {
|
|
440
|
+
out = path.join(os.homedir(), out.slice(2));
|
|
441
|
+
}
|
|
442
|
+
// ${VAR} expands from envOverrides (the spawned child's view) first,
|
|
443
|
+
// then process.env. ${VAR:-fallback} keeps the bash-style default
|
|
444
|
+
// so spec authors can say e.g. ${HERMES_HOME:-~/.hermes}/state.db
|
|
445
|
+
// and have it work both for coordinator-launched sessions (where
|
|
446
|
+
// HERMES_HOME is set to a tmpdir) and normal sessions.
|
|
447
|
+
out = out.replace(/\$\{([A-Z_][A-Z0-9_]*)(?::-(.*?))?\}/g, (_m, name, fallback) => {
|
|
448
|
+
const v = input.envOverrides?.[name] ?? process.env[name];
|
|
449
|
+
return v != null && v !== '' ? v : (fallback ?? '');
|
|
450
|
+
});
|
|
451
|
+
// Re-expand ~ in case the fallback used it.
|
|
452
|
+
if (out.startsWith('~/')) out = path.join(os.homedir(), out.slice(2));
|
|
453
|
+
const now = new Date();
|
|
454
|
+
// Claude writes per-cwd transcripts under the resolved path and replaces
|
|
455
|
+
// every non-alphanumeric project-path character except `_` and `-` with
|
|
456
|
+
// `-`. Realpath also handles aliases such as /tmp -> /private/tmp.
|
|
457
|
+
const workspaceRaw = input.workspace ?? '';
|
|
458
|
+
let workspaceResolved = workspaceRaw;
|
|
459
|
+
// The caller may request the RAW slug (skip realpath) to recover from
|
|
460
|
+
// Windows realpath normalization diverging the {cwd*} slug from the dir
|
|
461
|
+
// the CLI actually created. Default keeps realpath (handles /tmp ->
|
|
462
|
+
// /private/tmp aliasing that the CLI itself resolves on macOS).
|
|
463
|
+
if (workspaceRaw && !opts?.skipWorkspaceRealpath) {
|
|
464
|
+
try { workspaceResolved = fs.realpathSync(workspaceRaw); }
|
|
465
|
+
catch { /* path may not exist yet — keep the raw value */ }
|
|
466
|
+
}
|
|
467
|
+
const vars: Record<string, string> = {
|
|
468
|
+
cwd: workspaceResolved,
|
|
469
|
+
cwd_dashed: workspaceResolved.replace(/\//g, '-'),
|
|
470
|
+
cwd_claude_project: claudeProjectDirName(workspaceResolved),
|
|
471
|
+
session_id: input.providerSessionId || input.sessionId || input.historySessionId || '',
|
|
472
|
+
yyyy: String(now.getFullYear()),
|
|
473
|
+
mm: String(now.getMonth() + 1).padStart(2, '0'),
|
|
474
|
+
dd: String(now.getDate()).padStart(2, '0'),
|
|
475
|
+
};
|
|
476
|
+
// Replace {var}. If a referenced variable is empty (e.g. session_id
|
|
477
|
+
// before the agent has allocated one), return null so the caller
|
|
478
|
+
// doesn't accidentally fall through to an unrelated newest-file
|
|
479
|
+
// match. Wildcards (`*`) are explicitly allowed to pass — the dir
|
|
480
|
+
// glob walker handles them separately.
|
|
481
|
+
let missing = false;
|
|
482
|
+
out = out.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_m, name) => {
|
|
483
|
+
const v = vars[name] ?? '';
|
|
484
|
+
if (!v) missing = true;
|
|
485
|
+
return v;
|
|
486
|
+
});
|
|
487
|
+
if (missing) return null;
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function claudeProjectDirName(workspace: string): string {
|
|
492
|
+
return workspace.replace(/[^A-Za-z0-9_-]/g, '-');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Last-resort lookup for a transcript by its exact session id, ignoring the
|
|
497
|
+
* per-cwd slug entirely. The slug-derived directory above can miss completely
|
|
498
|
+
* when the CLI's on-disk project dir disagrees with the slug we reconstruct
|
|
499
|
+
* (notably on Windows, where fs.realpathSync normalizes drive-letter case and
|
|
500
|
+
* adds a \\?\ prefix). Since the session id is a UUID, scanning the projects
|
|
501
|
+
* root for `<sessionId>.jsonl` is unambiguous.
|
|
502
|
+
*
|
|
503
|
+
* Derives the scan base from the template's segments up to (but excluding) the
|
|
504
|
+
* first one that references a per-session variable ({cwd*} or {session_id}) —
|
|
505
|
+
* e.g. `~/.claude/projects/{cwd_claude_project}/{session_id}.jsonl` → scan
|
|
506
|
+
* `~/.claude/projects`. Returns the matching file path, or null.
|
|
507
|
+
*/
|
|
508
|
+
function scanProjectsRootForSessionFile(template: string, input: NativeHistoryInput, requestedSessionId: string): string | null {
|
|
509
|
+
if (!requestedSessionId) return null;
|
|
510
|
+
// Resolve the leading static portion of the template (everything before the
|
|
511
|
+
// first {var} segment) into a concrete base directory.
|
|
512
|
+
let head = template;
|
|
513
|
+
if (head.startsWith('~/') || head === '~') head = path.join(os.homedir(), head.slice(2));
|
|
514
|
+
const segs = head.split('/');
|
|
515
|
+
const baseParts: string[] = [];
|
|
516
|
+
for (const seg of segs) {
|
|
517
|
+
if (/[{}*?]/.test(seg)) break;
|
|
518
|
+
baseParts.push(seg);
|
|
519
|
+
}
|
|
520
|
+
const base = baseParts.join('/');
|
|
521
|
+
if (!base) return null;
|
|
522
|
+
let baseStat: fs.Stats | null = null;
|
|
523
|
+
try { baseStat = fs.statSync(base); } catch { return null; }
|
|
524
|
+
if (!baseStat.isDirectory()) return null;
|
|
525
|
+
|
|
526
|
+
const needle = `${requestedSessionId.toLowerCase()}.jsonl`;
|
|
527
|
+
// Bounded walk: project layouts are <root>/<projectDir>/<uuid>.jsonl, so a
|
|
528
|
+
// shallow scan (root + one level of subdirs) suffices and avoids walking an
|
|
529
|
+
// unbounded tree. Check the root itself first, then each immediate subdir.
|
|
530
|
+
const dirsToScan: string[] = [base];
|
|
531
|
+
try {
|
|
532
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
533
|
+
if (entry.isDirectory()) dirsToScan.push(path.join(base, entry.name));
|
|
534
|
+
}
|
|
535
|
+
} catch { /* readdir failed — fall back to scanning base only */ }
|
|
536
|
+
|
|
537
|
+
for (const dir of dirsToScan) {
|
|
538
|
+
let entries: fs.Dirent[];
|
|
539
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
540
|
+
for (const entry of entries) {
|
|
541
|
+
if (!entry.isFile()) continue;
|
|
542
|
+
if (entry.name.toLowerCase() !== needle) continue;
|
|
543
|
+
const found = path.join(dir, entry.name);
|
|
544
|
+
LOG.debug('NativeHistory', `jsonl scan-fallback hit: sessionId=${requestedSessionId} resolved via projects-root scan → ${JSON.stringify(found)} (slug-derived path missed; likely realpath/slug divergence)`);
|
|
545
|
+
return found;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function globToRegex(pattern: string): RegExp {
|
|
552
|
+
// Minimal glob: `*` → `[^/]*`, `?` → `[^/]`, `.` → `\.`. Anchored.
|
|
553
|
+
const re = pattern
|
|
554
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
555
|
+
.replace(/\*/g, '[^/]*')
|
|
556
|
+
.replace(/\?/g, '[^/]');
|
|
557
|
+
return new RegExp(`^${re}$`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Resolve a path with `*` segments to all concrete directories that match,
|
|
562
|
+
* then pick the newest file inside any of them. `*` matches one path
|
|
563
|
+
* component (no slashes); `**` matches zero or more components. Filenames
|
|
564
|
+
* are matched against `pattern`, not the glob — file_pattern is the right
|
|
565
|
+
* place for the leaf match.
|
|
566
|
+
*/
|
|
567
|
+
function expandDirGlob(template: string): string[] {
|
|
568
|
+
const parts = template.split('/');
|
|
569
|
+
let dirs: string[] = parts[0] === '' ? ['/'] : [parts[0]];
|
|
570
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
571
|
+
const seg = parts[i];
|
|
572
|
+
if (!seg) continue;
|
|
573
|
+
const next: string[] = [];
|
|
574
|
+
if (seg === '**') {
|
|
575
|
+
for (const d of dirs) walkAllDirs(d, next);
|
|
576
|
+
dirs = next;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (seg.includes('*') || seg.includes('?')) {
|
|
580
|
+
const re = globToRegex(seg);
|
|
581
|
+
for (const d of dirs) {
|
|
582
|
+
let entries: fs.Dirent[];
|
|
583
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
584
|
+
for (const e of entries) {
|
|
585
|
+
if (e.isDirectory() && re.test(e.name)) next.push(path.join(d, e.name));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
for (const d of dirs) {
|
|
590
|
+
const candidate = path.join(d, seg);
|
|
591
|
+
let stat: fs.Stats | null = null;
|
|
592
|
+
try { stat = fs.statSync(candidate); } catch { continue; }
|
|
593
|
+
if (stat.isDirectory()) next.push(candidate);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
dirs = next;
|
|
597
|
+
}
|
|
598
|
+
return dirs;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function walkAllDirs(root: string, out: string[]): void {
|
|
602
|
+
let entries: fs.Dirent[];
|
|
603
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
|
|
604
|
+
out.push(root);
|
|
605
|
+
for (const e of entries) {
|
|
606
|
+
if (e.isDirectory()) walkAllDirs(path.join(root, e.name), out);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function newestRecentFileAcrossGlob(template: string, pattern: RegExp, windowMs: number, sessionFloorMs = 0): string | null {
|
|
611
|
+
const dirs = expandDirGlob(template);
|
|
612
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs);
|
|
613
|
+
let best: { p: string; mtime: number } | null = null;
|
|
614
|
+
for (const d of dirs) {
|
|
615
|
+
let entries: fs.Dirent[];
|
|
616
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
617
|
+
for (const e of entries) {
|
|
618
|
+
if (!e.isFile() || !pattern.test(e.name)) continue;
|
|
619
|
+
const p = path.join(d, e.name);
|
|
620
|
+
const mtime = safeMtimeMs(p);
|
|
621
|
+
if (mtime < cutoff) continue;
|
|
622
|
+
if (!best || mtime > best.mtime) best = { p, mtime };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return best ? best.p : null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function hasDateTemplateSegment(template: string): boolean {
|
|
629
|
+
return /\{yyyy\}|\{mm\}|\{dd\}/.test(template);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Walk nearby local calendar days of a date-templated path (e.g.
|
|
634
|
+
* `~/.codex/sessions/{yyyy}/{mm}/{dd}`) and return the newest matching
|
|
635
|
+
* file across all of them. Providers differ on local-vs-UTC date buckets,
|
|
636
|
+
* so today's expanded dir alone can miss a live transcript.
|
|
637
|
+
*/
|
|
638
|
+
function newestRecentFileAcrossDateWindow(
|
|
639
|
+
template: string,
|
|
640
|
+
input: NativeHistoryInput,
|
|
641
|
+
pattern: RegExp,
|
|
642
|
+
windowMs: number,
|
|
643
|
+
sessionFloorMs: number,
|
|
644
|
+
): string | null {
|
|
645
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs);
|
|
646
|
+
let best: { p: string; mtime: number } | null = null;
|
|
647
|
+
for (const dayOffset of [0, -1, 1, -2, 2]) {
|
|
648
|
+
const dayMs = Date.now() + dayOffset * 24 * 60 * 60 * 1000;
|
|
649
|
+
const dayInput: NativeHistoryInput = { ...input, sessionStartedAtMs: sessionFloorMs };
|
|
650
|
+
const resolved = expandPathForDate(template, dayInput, new Date(dayMs));
|
|
651
|
+
if (!resolved) continue;
|
|
652
|
+
let entries: fs.Dirent[];
|
|
653
|
+
try { entries = fs.readdirSync(resolved, { withFileTypes: true }); } catch { continue; }
|
|
654
|
+
for (const e of entries) {
|
|
655
|
+
if (!e.isFile() || !pattern.test(e.name)) continue;
|
|
656
|
+
const p = path.join(resolved, e.name);
|
|
657
|
+
const mtime = safeMtimeMs(p);
|
|
658
|
+
if (mtime < cutoff) continue;
|
|
659
|
+
if (!best || mtime > best.mtime) best = { p, mtime };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return best ? best.p : null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function expandPathForDate(template: string, input: NativeHistoryInput, day: Date): string | null {
|
|
666
|
+
// Reuse expandPath logic but stamp {yyyy}/{mm}/{dd} from the given day.
|
|
667
|
+
if (!template) return null;
|
|
668
|
+
let out = template;
|
|
669
|
+
if (out.startsWith('~/') || out === '~') {
|
|
670
|
+
out = path.join(os.homedir(), out.slice(2));
|
|
671
|
+
}
|
|
672
|
+
out = out.replace(/\$\{([A-Z_][A-Z0-9_]*)(?::-(.*?))?\}/g, (_m, name, fallback) => {
|
|
673
|
+
const v = input.envOverrides?.[name] ?? process.env[name];
|
|
674
|
+
return v != null && v !== '' ? v : (fallback ?? '');
|
|
675
|
+
});
|
|
676
|
+
if (out.startsWith('~/')) out = path.join(os.homedir(), out.slice(2));
|
|
677
|
+
const workspaceRaw = input.workspace ?? '';
|
|
678
|
+
let workspaceResolved = workspaceRaw;
|
|
679
|
+
if (workspaceRaw) {
|
|
680
|
+
try { workspaceResolved = fs.realpathSync(workspaceRaw); } catch { /* keep raw */ }
|
|
681
|
+
}
|
|
682
|
+
const vars: Record<string, string> = {
|
|
683
|
+
cwd: workspaceResolved,
|
|
684
|
+
cwd_dashed: workspaceResolved.replace(/\//g, '-'),
|
|
685
|
+
cwd_claude_project: claudeProjectDirName(workspaceResolved),
|
|
686
|
+
session_id: input.providerSessionId || input.sessionId || input.historySessionId || '',
|
|
687
|
+
yyyy: String(day.getFullYear()),
|
|
688
|
+
mm: String(day.getMonth() + 1).padStart(2, '0'),
|
|
689
|
+
dd: String(day.getDate()).padStart(2, '0'),
|
|
690
|
+
};
|
|
691
|
+
let missing = false;
|
|
692
|
+
out = out.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_m, name) => {
|
|
693
|
+
const v = vars[name] ?? '';
|
|
694
|
+
if (!v) missing = true;
|
|
695
|
+
return v;
|
|
696
|
+
});
|
|
697
|
+
if (missing) return null;
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function newestRecentFile(dir: string, pattern: RegExp, windowMs: number, sessionFloorMs = 0): string | null {
|
|
702
|
+
let entries: fs.Dirent[];
|
|
703
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return null; }
|
|
704
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs);
|
|
705
|
+
let best: { p: string; mtime: number } | null = null;
|
|
706
|
+
for (const e of entries) {
|
|
707
|
+
if (!e.isFile() || !pattern.test(e.name)) continue;
|
|
708
|
+
const p = path.join(dir, e.name);
|
|
709
|
+
const mtime = safeMtimeMs(p);
|
|
710
|
+
if (mtime < cutoff) continue;
|
|
711
|
+
if (!best || mtime > best.mtime) best = { p, mtime };
|
|
712
|
+
}
|
|
713
|
+
return best ? best.p : null;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function safeMtimeMs(p: string): number {
|
|
717
|
+
try { return Math.floor(fs.statSync(p).mtimeMs); } catch { return 0; }
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function readRequestedSessionId(input: NativeHistoryInput): string {
|
|
721
|
+
const raw = input.providerSessionId || input.sessionId || input.historySessionId || '';
|
|
722
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
723
|
+
return UUID_RE.test(value) ? value : '';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function filenameUuid(filePath: string): string {
|
|
727
|
+
const match = path.basename(filePath).match(UUID_RE);
|
|
728
|
+
return match?.[1] || '';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function pickExactSessionFile(dir: string, pattern: RegExp, requestedSessionId: string): string | null {
|
|
732
|
+
if (!requestedSessionId) return null;
|
|
733
|
+
const files = listMatchingFiles(dir, pattern)
|
|
734
|
+
.filter(p => filenameUuid(p).toLowerCase() === requestedSessionId.toLowerCase())
|
|
735
|
+
.sort((a, b) => safeMtimeMs(b) - safeMtimeMs(a));
|
|
736
|
+
return files[0] || null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function pickExactSessionFileAcrossGlob(template: string, pattern: RegExp, requestedSessionId: string): string | null {
|
|
740
|
+
if (!requestedSessionId) return null;
|
|
741
|
+
const dirs = expandDirGlob(template);
|
|
742
|
+
const matches: string[] = [];
|
|
743
|
+
for (const d of dirs) {
|
|
744
|
+
const found = pickExactSessionFile(d, pattern, requestedSessionId);
|
|
745
|
+
if (found) matches.push(found);
|
|
746
|
+
}
|
|
747
|
+
matches.sort((a, b) => safeMtimeMs(b) - safeMtimeMs(a));
|
|
748
|
+
return matches[0] || null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function pickExactSessionFileAcrossDateWindow(
|
|
752
|
+
template: string,
|
|
753
|
+
input: NativeHistoryInput,
|
|
754
|
+
pattern: RegExp,
|
|
755
|
+
requestedSessionId: string,
|
|
756
|
+
): string | null {
|
|
757
|
+
if (!requestedSessionId) return null;
|
|
758
|
+
const matches: string[] = [];
|
|
759
|
+
for (const dayOffset of [0, -1, 1, -2, 2]) {
|
|
760
|
+
const dayMs = Date.now() + dayOffset * 24 * 60 * 60 * 1000;
|
|
761
|
+
const resolved = expandPathForDate(template, input, new Date(dayMs));
|
|
762
|
+
if (!resolved) continue;
|
|
763
|
+
const found = pickExactSessionFile(resolved, pattern, requestedSessionId);
|
|
764
|
+
if (found) matches.push(found);
|
|
765
|
+
}
|
|
766
|
+
matches.sort((a, b) => safeMtimeMs(b) - safeMtimeMs(a));
|
|
767
|
+
return matches[0] || null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
771
|
+
// Per-session rollout binding
|
|
772
|
+
//
|
|
773
|
+
// Reads the first JSONL line of a candidate file and returns a `session_meta`
|
|
774
|
+
// payload if present. Codex-cli writes
|
|
775
|
+
// {"timestamp":"...","type":"session_meta","payload":{"id":...,"cwd":...,
|
|
776
|
+
// "timestamp":"..."}}
|
|
777
|
+
// as the first record. Other providers that don't follow this convention
|
|
778
|
+
// return null and fall back to the mtime-based picker.
|
|
779
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
780
|
+
|
|
781
|
+
interface CandidateMeta {
|
|
782
|
+
cwd?: string;
|
|
783
|
+
sessionTimestampMs?: number;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function readCandidateSessionMeta(filePath: string): CandidateMeta | null {
|
|
787
|
+
try {
|
|
788
|
+
// Read only the first line — meta is always the first JSONL record
|
|
789
|
+
// and full file reads here would scale O(files × file_size).
|
|
790
|
+
const fd = fs.openSync(filePath, 'r');
|
|
791
|
+
try {
|
|
792
|
+
const buf = Buffer.alloc(8192);
|
|
793
|
+
const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
794
|
+
if (bytes <= 0) return null;
|
|
795
|
+
const text = buf.subarray(0, bytes).toString('utf8');
|
|
796
|
+
const nl = text.indexOf('\n');
|
|
797
|
+
const firstLine = (nl >= 0 ? text.slice(0, nl) : text).trim();
|
|
798
|
+
if (!firstLine) return null;
|
|
799
|
+
const parsed = JSON.parse(firstLine) as Record<string, unknown>;
|
|
800
|
+
if (String(parsed.type ?? '') !== 'session_meta') return null;
|
|
801
|
+
const payload = parsed.payload && typeof parsed.payload === 'object'
|
|
802
|
+
? (parsed.payload as Record<string, unknown>)
|
|
803
|
+
: null;
|
|
804
|
+
if (!payload) return null;
|
|
805
|
+
const cwd = typeof payload.cwd === 'string' ? payload.cwd : undefined;
|
|
806
|
+
const tsRaw = payload.timestamp;
|
|
807
|
+
const tsMs = typeof tsRaw === 'string'
|
|
808
|
+
? Date.parse(tsRaw)
|
|
809
|
+
: typeof tsRaw === 'number'
|
|
810
|
+
? (tsRaw < 1e12 ? Math.floor(tsRaw * 1000) : Math.floor(tsRaw))
|
|
811
|
+
: NaN;
|
|
812
|
+
return {
|
|
813
|
+
cwd,
|
|
814
|
+
sessionTimestampMs: Number.isFinite(tsMs) ? tsMs : undefined,
|
|
815
|
+
};
|
|
816
|
+
} finally {
|
|
817
|
+
fs.closeSync(fd);
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function pickBoundFromEntries(
|
|
825
|
+
candidatePaths: string[],
|
|
826
|
+
sessionFloorMs: number,
|
|
827
|
+
workspaceHint: string,
|
|
828
|
+
): string | null {
|
|
829
|
+
if (!sessionFloorMs || !workspaceHint || candidatePaths.length === 0) return null;
|
|
830
|
+
// Resolve workspaceHint to handle macOS /tmp → /private/tmp aliasing, the
|
|
831
|
+
// same way expandPath does for the template substitution. Without this
|
|
832
|
+
// the daemon's `/Users/foo/repo` and codex's `/private/Users/foo/repo`
|
|
833
|
+
// never compare equal and disambiguation silently fails.
|
|
834
|
+
let workspaceResolved = workspaceHint;
|
|
835
|
+
try { workspaceResolved = fs.realpathSync(workspaceHint); } catch { /* keep raw */ }
|
|
836
|
+
let best: { p: string; diff: number } | null = null;
|
|
837
|
+
for (const p of candidatePaths) {
|
|
838
|
+
const meta = readCandidateSessionMeta(p);
|
|
839
|
+
if (!meta || !meta.cwd || meta.sessionTimestampMs == null) continue;
|
|
840
|
+
let candidateCwd = meta.cwd;
|
|
841
|
+
try { candidateCwd = fs.realpathSync(meta.cwd); } catch { /* keep raw */ }
|
|
842
|
+
if (candidateCwd !== workspaceResolved && meta.cwd !== workspaceHint) continue;
|
|
843
|
+
const diff = Math.abs(meta.sessionTimestampMs - sessionFloorMs);
|
|
844
|
+
if (diff > SPAWN_BIND_GRACE_MS) continue;
|
|
845
|
+
if (!best || diff < best.diff) best = { p, diff };
|
|
846
|
+
}
|
|
847
|
+
return best ? best.p : null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function listMatchingFiles(dir: string, pattern: RegExp): string[] {
|
|
851
|
+
let entries: fs.Dirent[];
|
|
852
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
853
|
+
const out: string[] = [];
|
|
854
|
+
for (const e of entries) {
|
|
855
|
+
if (!e.isFile() || !pattern.test(e.name)) continue;
|
|
856
|
+
out.push(path.join(dir, e.name));
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function pickSessionBoundFile(
|
|
862
|
+
dir: string,
|
|
863
|
+
pattern: RegExp,
|
|
864
|
+
windowMs: number,
|
|
865
|
+
sessionFloorMs: number,
|
|
866
|
+
workspaceHint: string,
|
|
867
|
+
): string | null {
|
|
868
|
+
if (!sessionFloorMs || !workspaceHint) return null;
|
|
869
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
|
|
870
|
+
const files = listMatchingFiles(dir, pattern).filter(p => safeMtimeMs(p) >= cutoff);
|
|
871
|
+
return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function pickSessionBoundFileAcrossGlob(
|
|
875
|
+
template: string,
|
|
876
|
+
pattern: RegExp,
|
|
877
|
+
windowMs: number,
|
|
878
|
+
sessionFloorMs: number,
|
|
879
|
+
workspaceHint: string,
|
|
880
|
+
): string | null {
|
|
881
|
+
if (!sessionFloorMs || !workspaceHint) return null;
|
|
882
|
+
const dirs = expandDirGlob(template);
|
|
883
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
|
|
884
|
+
const files: string[] = [];
|
|
885
|
+
for (const d of dirs) {
|
|
886
|
+
for (const p of listMatchingFiles(d, pattern)) {
|
|
887
|
+
if (safeMtimeMs(p) >= cutoff) files.push(p);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function pickSessionBoundFileAcrossDateWindow(
|
|
894
|
+
template: string,
|
|
895
|
+
input: NativeHistoryInput,
|
|
896
|
+
pattern: RegExp,
|
|
897
|
+
windowMs: number,
|
|
898
|
+
sessionFloorMs: number,
|
|
899
|
+
workspaceHint: string,
|
|
900
|
+
): string | null {
|
|
901
|
+
if (!sessionFloorMs || !workspaceHint) return null;
|
|
902
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
|
|
903
|
+
const files: string[] = [];
|
|
904
|
+
for (const dayOffset of [0, -1, 1, -2, 2]) {
|
|
905
|
+
const dayMs = sessionFloorMs + dayOffset * 24 * 60 * 60 * 1000;
|
|
906
|
+
const dayInput: NativeHistoryInput = { ...input, sessionStartedAtMs: sessionFloorMs };
|
|
907
|
+
const resolved = expandPathForDate(template, dayInput, new Date(dayMs));
|
|
908
|
+
if (!resolved) continue;
|
|
909
|
+
for (const p of listMatchingFiles(resolved, pattern)) {
|
|
910
|
+
if (safeMtimeMs(p) >= cutoff) files.push(p);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
917
|
+
// jsonpath-lite + projection
|
|
918
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Resolve `$.a.b[0].c` against a record. Strings without leading `$` are
|
|
922
|
+
* literals. Supports `||` fallback between paths so a single message_map
|
|
923
|
+
* entry can pick the first non-empty value across alternative locations
|
|
924
|
+
* (e.g. agy's content vs. thinking).
|
|
925
|
+
*/
|
|
926
|
+
function jsonPathGet(record: any, expr: string): unknown {
|
|
927
|
+
if (typeof expr !== 'string') return undefined;
|
|
928
|
+
if (expr.includes('||')) {
|
|
929
|
+
for (const alt of expr.split('||')) {
|
|
930
|
+
const v = jsonPathGet(record, alt.trim());
|
|
931
|
+
if (v != null && v !== '') return v;
|
|
932
|
+
}
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
if (!expr.startsWith('$')) return expr;
|
|
936
|
+
let cur: any = record;
|
|
937
|
+
let i = 1;
|
|
938
|
+
while (i < expr.length && cur != null) {
|
|
939
|
+
const ch = expr[i];
|
|
940
|
+
if (ch === '.') { i += 1; continue; }
|
|
941
|
+
if (ch === '[') {
|
|
942
|
+
const close = expr.indexOf(']', i);
|
|
943
|
+
if (close < 0) return undefined;
|
|
944
|
+
const idx = Number(expr.slice(i + 1, close));
|
|
945
|
+
if (!Number.isInteger(idx)) return undefined;
|
|
946
|
+
cur = cur[idx];
|
|
947
|
+
i = close + 1;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
let end = i;
|
|
951
|
+
while (end < expr.length && expr[end] !== '.' && expr[end] !== '[') end += 1;
|
|
952
|
+
const key = expr.slice(i, end);
|
|
953
|
+
cur = cur[key];
|
|
954
|
+
i = end;
|
|
955
|
+
}
|
|
956
|
+
return cur;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Project one on-disk record into zero or more transcript messages.
|
|
961
|
+
*
|
|
962
|
+
* A record yields at most one text bubble (the prose turn) plus — when the
|
|
963
|
+
* spec declares `message_map.tools` — one `kind:'tool'` bubble per tool-call
|
|
964
|
+
* or tool-result content block. Without `tools`, behaviour is identical to
|
|
965
|
+
* the old single-message projection: text-only, tool blocks dropped.
|
|
966
|
+
*/
|
|
967
|
+
function projectMessages(record: any, map: NativeHistoryMessageMap, index: number, total: number, sourceMtimeMs: number): NativeHistoryMessage[] {
|
|
968
|
+
const roleRaw = jsonPathGet(record, map.role);
|
|
969
|
+
const role = normalizeRole(roleRaw);
|
|
970
|
+
|
|
971
|
+
// Records are passed in chronological order (oldest → newest), so the
|
|
972
|
+
// last record's receivedAt should be ~sourceMtimeMs (when the file was
|
|
973
|
+
// last touched) and earlier records should walk backwards. Earlier
|
|
974
|
+
// version had this inverted, which made the dashboard render bubbles
|
|
975
|
+
// in reverse order and produce the "chat jumping" effect.
|
|
976
|
+
let receivedAt = sourceMtimeMs - ((total - 1 - index) * 1000);
|
|
977
|
+
if (map.timestamp_ms) {
|
|
978
|
+
const tsRaw = jsonPathGet(record, map.timestamp_ms);
|
|
979
|
+
const parsed = parseTimestamp(tsRaw);
|
|
980
|
+
if (parsed != null) receivedAt = parsed;
|
|
981
|
+
}
|
|
982
|
+
const kindRaw = map.kind ? jsonPathGet(record, map.kind) : undefined;
|
|
983
|
+
const kind = typeof kindRaw === 'string' && kindRaw ? kindRaw : 'standard';
|
|
984
|
+
|
|
985
|
+
const out: NativeHistoryMessage[] = [];
|
|
986
|
+
|
|
987
|
+
// Two transcript shapes carry tool activity:
|
|
988
|
+
// - record-level: the whole record IS a tool call/result (codex stores
|
|
989
|
+
// each function_call / function_call_output as its own jsonl record).
|
|
990
|
+
// - block-nested: tool blocks live inside the message's content array
|
|
991
|
+
// (claude stores tool_use / tool_result as content blocks).
|
|
992
|
+
// When the spec opts into `tools`, try the record itself first; if it's a
|
|
993
|
+
// tool record we emit only that bubble (it has no prose). Otherwise emit
|
|
994
|
+
// the text bubble plus a tool bubble per matching content block.
|
|
995
|
+
if (map.tools) {
|
|
996
|
+
const recordTool = projectToolBlock(record, role, map.tools);
|
|
997
|
+
if (recordTool) {
|
|
998
|
+
out.push({ ...recordTool, receivedAt });
|
|
999
|
+
return out;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const contentRaw = jsonPathGet(record, map.content);
|
|
1004
|
+
const content = cleanContent(stringifyContent(contentRaw), map);
|
|
1005
|
+
if (content) out.push({ role, content, receivedAt, kind });
|
|
1006
|
+
|
|
1007
|
+
// Block-nested tool bubbles are ordered just after the text bubble of the
|
|
1008
|
+
// same record by nudging receivedAt forward a millisecond per bubble, so a
|
|
1009
|
+
// turn's prose still renders before its tool activity without colliding
|
|
1010
|
+
// with the next record's timestamp.
|
|
1011
|
+
if (map.tools && Array.isArray(contentRaw)) {
|
|
1012
|
+
let nudge = 1;
|
|
1013
|
+
for (const block of contentRaw) {
|
|
1014
|
+
const tool = projectToolBlock(block, role, map.tools);
|
|
1015
|
+
if (tool) {
|
|
1016
|
+
out.push({ ...tool, receivedAt: receivedAt + nudge });
|
|
1017
|
+
nudge += 1;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** Apply content_strip / content_unwrap tag surgery and trim. */
|
|
1026
|
+
function cleanContent(input: string, map: NativeHistoryMessageMap): string {
|
|
1027
|
+
let content = input;
|
|
1028
|
+
if (content && map.content_strip) {
|
|
1029
|
+
for (const tag of map.content_strip) {
|
|
1030
|
+
const safeTag = tag.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
1031
|
+
const re = new RegExp(`<${safeTag}\\b[^>]*>[\\s\\S]*?<\\/${safeTag}\\s*>`, 'gi');
|
|
1032
|
+
content = content.replace(re, '');
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (content && map.content_unwrap) {
|
|
1036
|
+
for (const tag of map.content_unwrap) {
|
|
1037
|
+
const safeTag = tag.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
1038
|
+
const open = new RegExp(`<${safeTag}\\b[^>]*>`, 'gi');
|
|
1039
|
+
const close = new RegExp(`<\\/${safeTag}\\s*>`, 'gi');
|
|
1040
|
+
content = content.replace(open, '').replace(close, '');
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return content ? content.trim() : '';
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const DEFAULT_TOOL_CALL_TYPES = ['tool_use', 'function_call', 'custom_tool_call'];
|
|
1047
|
+
const DEFAULT_TOOL_RESULT_TYPES = ['tool_result', 'function_call_output', 'custom_tool_call_output'];
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Turn a single content block into a `kind:'tool'` message, or null if the
|
|
1051
|
+
* block is not a tool call/result. Field locations come from the spec's
|
|
1052
|
+
* `tools` map with Anthropic-block defaults.
|
|
1053
|
+
*
|
|
1054
|
+
* Both tool calls and tool results render on the assistant side: a tool call
|
|
1055
|
+
* is the agent's action, and a tool result is part of the agent's work, not a
|
|
1056
|
+
* user turn (claude/codex persist results under the user / no role, which would
|
|
1057
|
+
* otherwise misattribute them). Calls render as `↗ {name}: {one-line args}`,
|
|
1058
|
+
* results as `↘ {one-line result}`. The `role` param is accepted for symmetry
|
|
1059
|
+
* but tool bubbles are always assistant.
|
|
1060
|
+
*/
|
|
1061
|
+
function projectToolBlock(block: any, role: 'user' | 'assistant' | 'system', tmap: NativeHistoryToolMap): NativeHistoryMessage | null {
|
|
1062
|
+
void role;
|
|
1063
|
+
if (block == null || typeof block !== 'object') return null;
|
|
1064
|
+
const typeVal = String(jsonPathGet(block, tmap.block_type || '$.type') ?? '');
|
|
1065
|
+
if (!typeVal) return null;
|
|
1066
|
+
const callTypes = tmap.call_types ?? DEFAULT_TOOL_CALL_TYPES;
|
|
1067
|
+
const resultTypes = tmap.result_types ?? DEFAULT_TOOL_RESULT_TYPES;
|
|
1068
|
+
|
|
1069
|
+
if (callTypes.includes(typeVal)) {
|
|
1070
|
+
const name = String(jsonPathGet(block, tmap.call_name || '$.name') ?? 'tool').trim() || 'tool';
|
|
1071
|
+
const args = oneLine(stringifyContent(jsonPathGet(block, tmap.call_args || '$.input')), 240);
|
|
1072
|
+
const content = args ? `↗ ${name}: ${args}` : `↗ ${name}`;
|
|
1073
|
+
return { role: 'assistant', content, receivedAt: 0, kind: 'tool' };
|
|
1074
|
+
}
|
|
1075
|
+
if (resultTypes.includes(typeVal)) {
|
|
1076
|
+
const result = oneLine(stringifyContent(jsonPathGet(block, tmap.result_content || '$.content')), 600);
|
|
1077
|
+
if (!result) return null;
|
|
1078
|
+
return { role: 'assistant', content: `↘ ${result}`, receivedAt: 0, kind: 'tool' };
|
|
1079
|
+
}
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** Collapse whitespace to single spaces and cap length for a tool summary. */
|
|
1084
|
+
function oneLine(s: string, max: number): string {
|
|
1085
|
+
const flat = s.replace(/\s+/g, ' ').trim();
|
|
1086
|
+
return flat.length > max ? flat.slice(0, max - 1) + '…' : flat;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Coerce a timestamp value to epoch milliseconds. Accepts:
|
|
1091
|
+
* - number (ms or seconds — heuristic: < 1e12 means seconds)
|
|
1092
|
+
* - ISO 8601 string ("2026-06-05T01:25:28Z")
|
|
1093
|
+
* - numeric string
|
|
1094
|
+
*
|
|
1095
|
+
* Returns null when the value can't be parsed; caller falls back to the
|
|
1096
|
+
* monotonic-by-index estimate.
|
|
1097
|
+
*/
|
|
1098
|
+
function parseTimestamp(v: unknown): number | null {
|
|
1099
|
+
if (v == null) return null;
|
|
1100
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0) {
|
|
1101
|
+
return v < 1e12 ? Math.floor(v * 1000) : Math.floor(v);
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof v === 'string' && v) {
|
|
1104
|
+
const trimmed = v.trim();
|
|
1105
|
+
const asNum = Number(trimmed);
|
|
1106
|
+
if (Number.isFinite(asNum) && asNum > 0) {
|
|
1107
|
+
return asNum < 1e12 ? Math.floor(asNum * 1000) : Math.floor(asNum);
|
|
1108
|
+
}
|
|
1109
|
+
const parsed = Date.parse(trimmed);
|
|
1110
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1111
|
+
}
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function normalizeRole(r: unknown): 'user' | 'assistant' | 'system' {
|
|
1116
|
+
const s = String(r ?? '').toLowerCase();
|
|
1117
|
+
if (s === 'user' || s === 'human' || s === 'user_explicit') return 'user';
|
|
1118
|
+
if (s === 'assistant' || s === 'ai' || s === 'model') return 'assistant';
|
|
1119
|
+
if (s === 'tool' || s === 'tool_result' || s === 'function') return 'assistant';
|
|
1120
|
+
return 'system';
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Coerce a content value to a plain string the dashboard can render.
|
|
1125
|
+
*
|
|
1126
|
+
* Many providers ship structured content (claude messages are arrays of
|
|
1127
|
+
* typed blocks: text / tool_use / tool_result). We collapse those to
|
|
1128
|
+
* their text-bearing parts so the dashboard doesn't show raw JSON
|
|
1129
|
+
* fragments in the transcript. Tool calls/results are intentionally
|
|
1130
|
+
* dropped — the daemon's chat schema is for user-visible turns.
|
|
1131
|
+
*
|
|
1132
|
+
* Order of attempts:
|
|
1133
|
+
* 1. string → as-is
|
|
1134
|
+
* 2. array of blocks → join the `text` field of each
|
|
1135
|
+
* block that has one; if none have
|
|
1136
|
+
* a text field, fall through
|
|
1137
|
+
* 3. object with a top-level `text` → that string
|
|
1138
|
+
* 4. last resort → JSON.stringify
|
|
1139
|
+
*/
|
|
1140
|
+
function stringifyContent(v: unknown): string {
|
|
1141
|
+
if (v == null) return '';
|
|
1142
|
+
if (typeof v === 'string') return v;
|
|
1143
|
+
if (Array.isArray(v)) {
|
|
1144
|
+
const parts: string[] = [];
|
|
1145
|
+
for (const block of v) {
|
|
1146
|
+
if (block == null) continue;
|
|
1147
|
+
if (typeof block === 'string') { parts.push(block); continue; }
|
|
1148
|
+
if (typeof block === 'object') {
|
|
1149
|
+
const t = (block as any).text;
|
|
1150
|
+
if (typeof t === 'string' && t) { parts.push(t); continue; }
|
|
1151
|
+
// tool_use / tool_result / image / etc — skip from the
|
|
1152
|
+
// user-facing transcript. They re-surface via the
|
|
1153
|
+
// adapter's tool-event channel if/when that's wired.
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (parts.length > 0) return parts.join('\n');
|
|
1157
|
+
return '';
|
|
1158
|
+
}
|
|
1159
|
+
if (typeof v === 'object') {
|
|
1160
|
+
const t = (v as any).text;
|
|
1161
|
+
if (typeof t === 'string') return t;
|
|
1162
|
+
}
|
|
1163
|
+
try { return JSON.stringify(v); } catch { return String(v); }
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1167
|
+
// where-clause mini-language
|
|
1168
|
+
//
|
|
1169
|
+
// Grammar (intentionally tiny — keep spec readable):
|
|
1170
|
+
// expr := term (('&&' | '||') term)*
|
|
1171
|
+
// term := path op literal
|
|
1172
|
+
// op := '==' | '!=' | '>' | '<' | '>=' | '<='
|
|
1173
|
+
// path := jsonpath like '$.foo.bar[0].baz'
|
|
1174
|
+
// literal := string ('"…"' or "'…'") | number | true | false | null
|
|
1175
|
+
//
|
|
1176
|
+
// No grouping, no negation. If you need more, write a real reader file
|
|
1177
|
+
// behind `native_history.override_path`.
|
|
1178
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1179
|
+
|
|
1180
|
+
interface WhereTerm { path: string; op: string; lit: unknown; negate?: boolean }
|
|
1181
|
+
|
|
1182
|
+
function compileWhere(src: string): (record: any) => boolean {
|
|
1183
|
+
const ors: WhereTerm[][] = [];
|
|
1184
|
+
for (const orChunk of src.split('||')) {
|
|
1185
|
+
const ands: WhereTerm[] = [];
|
|
1186
|
+
for (const andChunk of orChunk.split('&&')) {
|
|
1187
|
+
const term = parseTerm(andChunk.trim());
|
|
1188
|
+
if (term) ands.push(term);
|
|
1189
|
+
}
|
|
1190
|
+
if (ands.length > 0) ors.push(ands);
|
|
1191
|
+
}
|
|
1192
|
+
return (record: any) => ors.some(ands => ands.every(t => evalTerm(t, record)));
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function parseTerm(src: string): WhereTerm | null {
|
|
1196
|
+
let s = src.trim();
|
|
1197
|
+
let negate = false;
|
|
1198
|
+
if (s.startsWith('!')) { negate = true; s = s.slice(1).trim(); }
|
|
1199
|
+
// Function-call form: startsWith($.x, "y") / endsWith($.x, "y") / contains($.x, "y")
|
|
1200
|
+
const fnMatch = s.match(/^(startsWith|endsWith|contains)\s*\(\s*(.+?)\s*,\s*(.+?)\s*\)$/);
|
|
1201
|
+
if (fnMatch) {
|
|
1202
|
+
const [, op, pathExpr, litExpr] = fnMatch;
|
|
1203
|
+
return { path: pathExpr, op, lit: parseLiteral(litExpr), negate };
|
|
1204
|
+
}
|
|
1205
|
+
// Binary comparison: <path> <op> <literal>
|
|
1206
|
+
const opMatch = s.match(/^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
|
|
1207
|
+
if (!opMatch) return null;
|
|
1208
|
+
const [, lhs, op, rhsRaw] = opMatch;
|
|
1209
|
+
return { path: lhs.trim(), op, lit: parseLiteral(rhsRaw.trim()), negate };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function parseLiteral(src: string): unknown {
|
|
1213
|
+
if (src === 'true') return true;
|
|
1214
|
+
if (src === 'false') return false;
|
|
1215
|
+
if (src === 'null') return null;
|
|
1216
|
+
if ((src.startsWith('"') && src.endsWith('"')) || (src.startsWith("'") && src.endsWith("'"))) {
|
|
1217
|
+
return src.slice(1, -1);
|
|
1218
|
+
}
|
|
1219
|
+
const n = Number(src);
|
|
1220
|
+
if (!Number.isNaN(n)) return n;
|
|
1221
|
+
return src; // bareword — treated as string
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function evalTerm(t: WhereTerm, record: any): boolean {
|
|
1225
|
+
const lhs = jsonPathGet(record, t.path);
|
|
1226
|
+
const lit = t.lit;
|
|
1227
|
+
let result: boolean;
|
|
1228
|
+
switch (t.op) {
|
|
1229
|
+
case '==': result = lhs === lit; break;
|
|
1230
|
+
case '!=': result = lhs !== lit; break;
|
|
1231
|
+
case '>': result = Number(lhs) > Number(lit); break;
|
|
1232
|
+
case '<': result = Number(lhs) < Number(lit); break;
|
|
1233
|
+
case '>=': result = Number(lhs) >= Number(lit); break;
|
|
1234
|
+
case '<=': result = Number(lhs) <= Number(lit); break;
|
|
1235
|
+
case 'startsWith': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.startsWith(lit); break;
|
|
1236
|
+
case 'endsWith': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.endsWith(lit); break;
|
|
1237
|
+
case 'contains': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.includes(lit); break;
|
|
1238
|
+
default: result = false;
|
|
1239
|
+
}
|
|
1240
|
+
return t.negate ? !result : result;
|
|
1241
|
+
}
|