@sayknow-cli/coding-agent 0.3.15 → 0.4.0
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/CHANGELOG.md +52 -0
- package/package.json +133 -116
- package/scripts/build-sdk-package-smoke.ts +132 -0
- package/scripts/capture-notifications-settings-showcase.ts +435 -0
- package/scripts/compile-args.ts +5 -2
- package/scripts/g005-daemon-smoke.ts +4 -5
- package/scripts/g011-daemon-path-smoke.ts +5 -5
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
- package/scripts/generate-sdk-operation-inventory.ts +914 -0
- package/scripts/generate-telegram-baseline-manifest.ts +128 -0
- package/scripts/manifest-schema.ts +124 -0
- package/scripts/run-test-manifest.ts +153 -0
- package/scripts/run-threat-model-check.ts +217 -0
- package/scripts/verify-skc-sdk-canonicalization.ts +3647 -0
- package/scripts/verify-skc-sdk-rename.ts +90 -0
- package/src/append-only-mode.ts +1 -1
- package/src/async/job-manager.ts +455 -69
- package/src/cli/args.ts +43 -12
- package/src/cli/auth-gateway-cli.ts +2 -2
- package/src/cli/config-cli.ts +77 -3
- package/src/cli/daemon-cli.ts +63 -14
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/notify-cli.ts +373 -283
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +31 -11
- package/src/commands/config.ts +1 -1
- package/src/commands/daemon.ts +50 -6
- package/src/commands/gc.ts +11 -1
- package/src/commands/harness.ts +127 -142
- package/src/commands/launch.ts +11 -6
- package/src/commands/mcp-serve.ts +97 -13
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +44 -7
- package/src/commands/sdk.ts +453 -0
- package/src/commands/ultragoal.ts +4 -1
- package/src/commands/worktree.ts +10 -53
- package/src/commit/agentic/agent.ts +1 -0
- package/src/config/atomic-yaml-patch.ts +490 -0
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-profile-activation.ts +233 -86
- package/src/config/model-profiles.ts +52 -49
- package/src/config/model-registry.ts +5 -4
- package/src/config/model-resolver.ts +487 -410
- package/src/config/model-selector-value.ts +50 -0
- package/src/config/models-config-schema.ts +17 -23
- package/src/config/settings-schema.ts +372 -21
- package/src/config/settings.ts +1120 -182
- package/src/coordinator-mcp/model-preset.ts +74 -20
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +3120 -3111
- package/src/daemon/builtin.ts +8 -5
- package/src/daemon/control-types.ts +1 -1
- package/src/daemon/runtime.ts +2 -2
- package/src/debug/index.ts +1 -1
- package/src/defaults/skc/skills/team/SKILL.md +27 -13
- package/src/defaults/skc/skills/ultragoal/SKILL.md +0 -2
- package/src/discovery/helpers.ts +1 -50
- package/src/discovery/mcp-json.ts +327 -20
- package/src/edit/index.ts +64 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/index.ts +2 -2
- package/src/eval/py/kernel.ts +5 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +52 -18
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/compact-handler.ts +3 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -2
- package/src/extensibility/extensions/loader.ts +77 -5
- package/src/extensibility/extensions/runner.ts +124 -7
- package/src/extensibility/extensions/types.ts +215 -18
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/extensibility/skc-plugins/installer.ts +16 -16
- package/src/extensibility/slash-commands.ts +3 -3
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/operate.ts +9 -9
- package/src/harness-control-plane/owner.ts +341 -41
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/receipts.ts +0 -1
- package/src/harness-control-plane/sdk-transport.ts +395 -0
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/harness-control-plane/session-transport.ts +94 -0
- package/src/harness-control-plane/types.ts +1 -1
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/client.ts +65 -2
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/i18n/messages/en.ts +1 -0
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +44 -37
- package/src/internal-urls/local-protocol.ts +420 -18
- package/src/lsp/client.ts +43 -7
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +7 -91
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +451 -236
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/DESIGN.md +258 -0
- package/src/modes/acp/acp-agent.ts +1236 -2612
- package/src/modes/acp/acp-client-bridge.ts +2 -20
- package/src/modes/acp/acp-event-mapper.ts +18 -2
- package/src/modes/acp/acp-mode.ts +8 -36
- package/src/modes/acp/permission-mode.ts +25 -0
- package/src/modes/acp/startup-options.ts +6 -0
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/bridge/bridge-mode.ts +6 -4
- package/src/modes/components/agent-dashboard.ts +37 -17
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +105 -33
- package/src/modes/components/custom-provider-wizard.ts +77 -15
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-selector.ts +93 -35
- package/src/modes/components/notifications-settings-editor.ts +1403 -0
- package/src/modes/components/pet-capability.ts +115 -0
- package/src/modes/components/pet-selector.ts +15 -23
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/sayknow-pet-widget.ts +169 -20
- package/src/modes/components/session-observer-overlay.ts +213 -775
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +89 -12
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/tool-transcript-format.ts +393 -0
- package/src/modes/components/transcript-viewer-overlay.ts +446 -0
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/command-controller.ts +25 -17
- package/src/modes/controllers/event-controller.ts +14 -18
- package/src/modes/controllers/extension-ui-controller.ts +441 -37
- package/src/modes/controllers/goal-mode-controller.ts +493 -0
- package/src/modes/controllers/input-controller.ts +889 -263
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +519 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +957 -54
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/index.ts +0 -22
- package/src/modes/interactive-mode.ts +376 -1130
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/modes/rpc/rpc-mode.ts +3 -1
- package/src/modes/runtime-init.ts +320 -6
- package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
- package/src/modes/shared/agent-wire/command-dispatch.ts +3 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +65 -111
- package/src/modes/shared/agent-wire/event-contract.ts +7 -8
- package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
- package/src/modes/shared/agent-wire/event-observation.ts +16 -7
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1331 -89
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +19 -17
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +407 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +63 -21
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/notifications/index.ts +8 -2
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +3 -8
- package/src/prompts/agents/critic.md +3 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +2 -24
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +18 -94
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/lsp.md +3 -3
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/client.ts +94 -19
- package/src/runtime-mcp/config.ts +27 -15
- package/src/runtime-mcp/discoverable-tool-metadata.ts +16 -13
- package/src/runtime-mcp/manager.ts +359 -106
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/runtime-mcp/oauth-flow.ts +42 -12
- package/src/runtime-mcp/transports/http.ts +49 -19
- package/src/runtime-mcp/transports/stdio.ts +47 -28
- package/src/runtime-mcp/types.ts +10 -0
- package/src/sdk/acp/adapter.ts +418 -0
- package/src/sdk/acp/index.ts +2 -0
- package/src/sdk/broker/broker.ts +877 -0
- package/src/sdk/broker/discovery.ts +238 -0
- package/src/sdk/broker/ensure.ts +393 -0
- package/src/sdk/broker/identity.ts +42 -0
- package/src/sdk/broker/index.ts +8 -0
- package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
- package/src/sdk/broker/internal-source.bunfig.toml +1 -0
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +583 -0
- package/src/sdk/broker/lifecycle.ts +3413 -0
- package/src/sdk/broker/process-incarnation.ts +151 -0
- package/src/sdk/broker/runtime.ts +164 -0
- package/src/sdk/broker/session-index.ts +678 -0
- package/src/sdk/broker/state-version.ts +47 -0
- package/src/sdk/broker/transport.ts +125 -0
- package/src/sdk/bus/attachment-registry.ts +23 -0
- package/src/sdk/bus/chat-adapters.ts +147 -0
- package/src/sdk/bus/chat-command-policy.ts +107 -0
- package/src/sdk/bus/chat-daemon-cli.ts +278 -0
- package/src/sdk/bus/chat-daemon-control.ts +1101 -0
- package/src/sdk/bus/chat-daemon-runtime.ts +527 -0
- package/src/sdk/bus/chat-effect-journal.ts +363 -0
- package/src/sdk/bus/config-commands.ts +192 -0
- package/src/sdk/bus/config.ts +568 -0
- package/src/sdk/bus/conversation-store.ts +367 -0
- package/src/sdk/bus/daemon-paths.ts +34 -0
- package/src/sdk/bus/discord-conversation.ts +85 -0
- package/src/sdk/bus/discord-daemon.ts +2292 -0
- package/src/sdk/bus/discord-live-provider.ts +521 -0
- package/src/sdk/bus/discord-provider.ts +56 -0
- package/src/sdk/bus/engine.ts +100 -0
- package/src/sdk/bus/helpers.ts +135 -0
- package/src/sdk/bus/html-format.ts +518 -0
- package/src/sdk/bus/index.ts +4961 -0
- package/src/sdk/bus/lifecycle-commands.ts +291 -0
- package/src/sdk/bus/lifecycle-control-runtime.ts +1409 -0
- package/src/sdk/bus/lifecycle-orchestrator.ts +458 -0
- package/src/sdk/bus/managed-daemon.ts +163 -0
- package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
- package/src/sdk/bus/natives-augment.d.ts +63 -0
- package/src/sdk/bus/notification-orchestration.ts +535 -0
- package/src/sdk/bus/notification-service.ts +1174 -0
- package/src/sdk/bus/operator-runtime.ts +171 -0
- package/src/sdk/bus/rate-limit-pool.ts +276 -0
- package/src/sdk/bus/recent-activity.ts +279 -0
- package/src/sdk/bus/reply-sent-store.ts +134 -0
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-draft.ts +107 -0
- package/src/sdk/bus/rich-render.ts +135 -0
- package/src/sdk/bus/session-control.ts +439 -0
- package/src/sdk/bus/slack-conversation.ts +74 -0
- package/src/sdk/bus/slack-daemon.ts +2144 -0
- package/src/sdk/bus/slack-live-provider.ts +380 -0
- package/src/sdk/bus/slack-provider.ts +76 -0
- package/src/sdk/bus/telegram-cli.ts +194 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +292 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +27 -0
- package/src/sdk/bus/telegram-daemon-control.ts +642 -0
- package/src/sdk/bus/telegram-daemon.ts +6675 -0
- package/src/sdk/bus/telegram-reference.ts +436 -0
- package/src/sdk/bus/telegram-setup.ts +663 -0
- package/src/sdk/bus/threaded-inbound.ts +141 -0
- package/src/sdk/bus/threaded-render.ts +294 -0
- package/src/sdk/bus/topic-registry.ts +364 -0
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/session-cli.ts +275 -0
- package/src/sdk/client/client.ts +1 -0
- package/src/sdk/client/discovery.ts +108 -0
- package/src/sdk/client/index.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +388 -0
- package/src/sdk/host/control/index.ts +10 -0
- package/src/sdk/host/control/operations.ts +68 -0
- package/src/sdk/host/events.ts +91 -0
- package/src/sdk/host/host.ts +338 -0
- package/src/sdk/host/index.ts +4 -0
- package/src/sdk/host/query/cursor.ts +189 -0
- package/src/sdk/host/query/handlers.ts +531 -0
- package/src/sdk/host/query/index.ts +3 -0
- package/src/sdk/host/query/revision-store.ts +1149 -0
- package/src/sdk/host/reverse-leases.ts +325 -0
- package/src/sdk/host/types.ts +29 -0
- package/src/sdk/index.ts +14 -0
- package/src/sdk/lifecycle-session.ts +39 -0
- package/src/sdk/mcp/index.ts +1 -0
- package/src/sdk/mcp/server.ts +332 -0
- package/src/sdk/models.ts +168 -0
- package/src/sdk/protocol/adapter-validation.ts +48 -0
- package/src/sdk/protocol/operation-inventory.generated.json +4719 -0
- package/src/sdk/protocol/operation-registry.ts +312 -0
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +2869 -0
- package/src/sdk/startup-capability.ts +189 -0
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +5602 -2138
- package/src/session/agent-storage.ts +18 -0
- package/src/session/artifacts.ts +188 -23
- package/src/session/cache-economics.ts +25 -8
- package/src/session/context-estimation.ts +7 -2
- package/src/session/default-model-selection.ts +93 -0
- package/src/session/fallback-chain-controller.ts +186 -0
- package/src/session/internal/managed-session-scope.ts +2402 -0
- package/src/session/internal/managed-session-storage.ts +1559 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +2578 -392
- package/src/session/session-storage.ts +550 -40
- package/src/session/streaming-output.ts +6 -0
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/session/yield-queue.ts +20 -0
- package/src/setup/credential-auto-import.ts +291 -35
- package/src/setup/hermes/templates/operator-instructions.v1.md +4 -6
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skc-runtime/deep-interview-recorder.ts +70 -3
- package/src/skc-runtime/deep-interview-state.ts +441 -5
- package/src/skc-runtime/gc-render.ts +19 -1
- package/src/skc-runtime/gc-runtime.ts +68 -3
- package/src/skc-runtime/goal-mode-request.ts +55 -8
- package/src/skc-runtime/launch-worktree.ts +5 -5
- package/src/skc-runtime/linux-proc.ts +71 -0
- package/src/skc-runtime/managed-owner-admission.ts +352 -0
- package/src/skc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/skc-runtime/psmux-detect.ts +82 -4
- package/src/skc-runtime/ralplan-runtime.ts +74 -90
- package/src/skc-runtime/session-resolution.ts +18 -3
- package/src/skc-runtime/session-state-sidecar.ts +47 -138
- package/src/skc-runtime/state-argv.ts +292 -0
- package/src/skc-runtime/state-migrations.ts +1 -1
- package/src/skc-runtime/state-runtime.ts +387 -451
- package/src/skc-runtime/state-writer.ts +36 -40
- package/src/skc-runtime/team-launch.ts +319 -0
- package/src/skc-runtime/team-notify.ts +326 -0
- package/src/skc-runtime/team-runtime.ts +8 -2
- package/src/skc-runtime/team-store.ts +828 -0
- package/src/skc-runtime/team-workers.ts +962 -0
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +55 -28
- package/src/skc-runtime/tmux-owner-isolation.ts +136 -11
- package/src/skc-runtime/tmux-sessions.ts +23 -34
- package/src/skc-runtime/ultragoal-change-set.ts +172 -0
- package/src/skc-runtime/ultragoal-evidence.ts +874 -0
- package/src/skc-runtime/ultragoal-guard.ts +136 -18
- package/src/skc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/skc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/skc-runtime/ultragoal-runtime.ts +325 -1322
- package/src/skc-runtime/windows-powershell-command.ts +58 -0
- package/src/skc-runtime/workflow-cli-common.ts +30 -0
- package/src/skc-runtime/workflow-manifest.generated.json +58 -0
- package/src/skc-runtime/workflow-manifest.ts +18 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/workflow-mutation-guard.ts +661 -0
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +115 -41
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +223 -45
- package/src/task/index.ts +124 -56
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +20 -134
- package/src/tools/ask-answer-registry.ts +16 -2
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +40 -2
- package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
- package/src/tools/browser/benchmark/detector-report.ts +157 -0
- package/src/tools/browser/benchmark/run-suite.ts +39 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/launch.ts +127 -20
- package/src/tools/browser/profile-discovery.ts +63 -0
- package/src/tools/browser/profile-posture.ts +59 -0
- package/src/tools/browser/profile-reuse.ts +102 -0
- package/src/tools/browser/profile-warmup.ts +109 -0
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/registry.ts +87 -11
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +392 -142
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +8 -6
- package/src/tools/browser.ts +6 -0
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +88 -32
- package/src/tools/output-meta.ts +19 -7
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
- package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
- package/src/tools/read.ts +226 -65
- package/src/tools/resource-gc.ts +125 -36
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +129 -34
- package/src/tools/telegram-send.ts +2 -2
- package/src/utils/event-bus.ts +9 -4
- package/src/utils/file-mentions.ts +4 -4
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/index.ts +3 -3
- package/src/web/search/providers/openai-compatible.ts +76 -23
- package/dist/types/append-only-mode.d.ts +0 -31
- package/dist/types/async/index.d.ts +0 -2
- package/dist/types/async/job-manager.d.ts +0 -328
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/autoresearch/dashboard.d.ts +0 -4
- package/dist/types/autoresearch/git.d.ts +0 -36
- package/dist/types/autoresearch/helpers.d.ts +0 -24
- package/dist/types/autoresearch/index.d.ts +0 -2
- package/dist/types/autoresearch/state.d.ts +0 -17
- package/dist/types/autoresearch/storage.d.ts +0 -142
- package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
- package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
- package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
- package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
- package/dist/types/autoresearch/types.d.ts +0 -154
- package/dist/types/build-metadata.d.ts +0 -7
- package/dist/types/capability/context-file.d.ts +0 -17
- package/dist/types/capability/extension-module.d.ts +0 -15
- package/dist/types/capability/extension.d.ts +0 -28
- package/dist/types/capability/fs.d.ts +0 -20
- package/dist/types/capability/hook.d.ts +0 -19
- package/dist/types/capability/index.d.ts +0 -80
- package/dist/types/capability/instruction.d.ts +0 -17
- package/dist/types/capability/mcp.d.ts +0 -49
- package/dist/types/capability/prompt.d.ts +0 -15
- package/dist/types/capability/rule.d.ts +0 -69
- package/dist/types/capability/settings.d.ts +0 -15
- package/dist/types/capability/skill.d.ts +0 -36
- package/dist/types/capability/slash-command.d.ts +0 -17
- package/dist/types/capability/ssh.d.ts +0 -23
- package/dist/types/capability/system-prompt.d.ts +0 -15
- package/dist/types/capability/tool.d.ts +0 -19
- package/dist/types/capability/types.d.ts +0 -154
- package/dist/types/cli/agents-cli.d.ts +0 -12
- package/dist/types/cli/args.d.ts +0 -57
- package/dist/types/cli/auth-broker-cli.d.ts +0 -25
- package/dist/types/cli/auth-gateway-cli.d.ts +0 -18
- package/dist/types/cli/classify-install-target.d.ts +0 -8
- package/dist/types/cli/commands/init-xdg.d.ts +0 -1
- package/dist/types/cli/completion-cli.d.ts +0 -41
- package/dist/types/cli/config-cli.d.ts +0 -23
- package/dist/types/cli/daemon-cli.d.ts +0 -27
- package/dist/types/cli/fast-help.d.ts +0 -1
- package/dist/types/cli/file-processor.d.ts +0 -11
- package/dist/types/cli/fixture-report.d.ts +0 -20
- package/dist/types/cli/fixture-report.test.d.ts +0 -1
- package/dist/types/cli/grep-cli.d.ts +0 -17
- package/dist/types/cli/initial-message.d.ts +0 -17
- package/dist/types/cli/list-models.d.ts +0 -36
- package/dist/types/cli/local-provider-smoke.d.ts +0 -59
- package/dist/types/cli/malloc-env-guard.d.ts +0 -65
- package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
- package/dist/types/cli/mcp-cli.d.ts +0 -25
- package/dist/types/cli/migrate-cli.d.ts +0 -20
- package/dist/types/cli/nofile-limit.d.ts +0 -11
- package/dist/types/cli/notify-cli.d.ts +0 -39
- package/dist/types/cli/plugin-cli.d.ts +0 -34
- package/dist/types/cli/read-cli.d.ts +0 -4
- package/dist/types/cli/session-picker.d.ts +0 -4
- package/dist/types/cli/setup-cli.d.ts +0 -73
- package/dist/types/cli/shell-cli.d.ts +0 -8
- package/dist/types/cli/skills-cli.d.ts +0 -9
- package/dist/types/cli/ssh-cli.d.ts +0 -21
- package/dist/types/cli/stats-cli.d.ts +0 -17
- package/dist/types/cli/telegram-cli.d.ts +0 -13
- package/dist/types/cli/update-cli.d.ts +0 -108
- package/dist/types/cli/web-search-cli.d.ts +0 -32
- package/dist/types/cli/worktree-cli.d.ts +0 -26
- package/dist/types/cli.d.ts +0 -140
- package/dist/types/commands/acp.d.ts +0 -12
- package/dist/types/commands/agents.d.ts +0 -34
- package/dist/types/commands/auth-broker.d.ts +0 -54
- package/dist/types/commands/auth-gateway.d.ts +0 -32
- package/dist/types/commands/codex-native-hook.d.ts +0 -6
- package/dist/types/commands/commit.d.ts +0 -27
- package/dist/types/commands/completion.d.ts +0 -29
- package/dist/types/commands/config.d.ts +0 -33
- package/dist/types/commands/contribution-prep.d.ts +0 -18
- package/dist/types/commands/coordinator.d.ts +0 -19
- package/dist/types/commands/daemon.d.ts +0 -45
- package/dist/types/commands/deep-interview.d.ts +0 -54
- package/dist/types/commands/gc.d.ts +0 -26
- package/dist/types/commands/grep.d.ts +0 -42
- package/dist/types/commands/harness.d.ts +0 -43
- package/dist/types/commands/launch.d.ts +0 -131
- package/dist/types/commands/local-provider.d.ts +0 -34
- package/dist/types/commands/mcp-serve.d.ts +0 -24
- package/dist/types/commands/mcp.d.ts +0 -70
- package/dist/types/commands/migrate.d.ts +0 -33
- package/dist/types/commands/notify.d.ts +0 -47
- package/dist/types/commands/plugin.d.ts +0 -58
- package/dist/types/commands/ralplan.d.ts +0 -7
- package/dist/types/commands/read.d.ts +0 -15
- package/dist/types/commands/rlm.d.ts +0 -10
- package/dist/types/commands/session.d.ts +0 -30
- package/dist/types/commands/setup.d.ts +0 -105
- package/dist/types/commands/shell.d.ts +0 -21
- package/dist/types/commands/skills.d.ts +0 -26
- package/dist/types/commands/ssh.d.ts +0 -48
- package/dist/types/commands/state.d.ts +0 -7
- package/dist/types/commands/stats.d.ts +0 -25
- package/dist/types/commands/team.d.ts +0 -24
- package/dist/types/commands/telegram.d.ts +0 -22
- package/dist/types/commands/ultragoal.d.ts +0 -8
- package/dist/types/commands/update.d.ts +0 -20
- package/dist/types/commands/web-search.d.ts +0 -87
- package/dist/types/commands/worktree.d.ts +0 -34
- package/dist/types/commit/agentic/agent.d.ts +0 -31
- package/dist/types/commit/agentic/fallback.d.ts +0 -5
- package/dist/types/commit/agentic/index.d.ts +0 -2
- package/dist/types/commit/agentic/state.d.ts +0 -58
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +0 -19
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +0 -10
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +0 -9
- package/dist/types/commit/agentic/tools/git-overview.d.ts +0 -9
- package/dist/types/commit/agentic/tools/index.d.ts +0 -16
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +0 -28
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +0 -36
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +0 -7
- package/dist/types/commit/agentic/tools/schemas.d.ts +0 -27
- package/dist/types/commit/agentic/tools/split-commit.d.ts +0 -53
- package/dist/types/commit/agentic/topo-sort.d.ts +0 -4
- package/dist/types/commit/agentic/trivial.d.ts +0 -7
- package/dist/types/commit/agentic/validation.d.ts +0 -20
- package/dist/types/commit/analysis/conventional.d.ts +0 -22
- package/dist/types/commit/analysis/index.d.ts +0 -4
- package/dist/types/commit/analysis/scope.d.ts +0 -6
- package/dist/types/commit/analysis/summary.d.ts +0 -19
- package/dist/types/commit/analysis/validation.d.ts +0 -8
- package/dist/types/commit/changelog/detect.d.ts +0 -2
- package/dist/types/commit/changelog/generate.d.ts +0 -30
- package/dist/types/commit/changelog/index.d.ts +0 -30
- package/dist/types/commit/changelog/parse.d.ts +0 -2
- package/dist/types/commit/cli.d.ts +0 -3
- package/dist/types/commit/git/diff.d.ts +0 -5
- package/dist/types/commit/index.d.ts +0 -4
- package/dist/types/commit/map-reduce/index.d.ts +0 -28
- package/dist/types/commit/map-reduce/map-phase.d.ts +0 -17
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +0 -13
- package/dist/types/commit/map-reduce/utils.d.ts +0 -2
- package/dist/types/commit/message.d.ts +0 -2
- package/dist/types/commit/model-selection.d.ts +0 -15
- package/dist/types/commit/pipeline.d.ts +0 -5
- package/dist/types/commit/shared-llm.d.ts +0 -54
- package/dist/types/commit/types.d.ts +0 -78
- package/dist/types/commit/utils/exclusions.d.ts +0 -4
- package/dist/types/commit/utils.d.ts +0 -20
- package/dist/types/config/config-file.d.ts +0 -58
- package/dist/types/config/file-lock-gc.d.ts +0 -5
- package/dist/types/config/file-lock.d.ts +0 -36
- package/dist/types/config/keybindings.d.ts +0 -364
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-profile-activation.d.ts +0 -65
- package/dist/types/config/model-profiles.d.ts +0 -39
- package/dist/types/config/model-registry.d.ts +0 -473
- package/dist/types/config/model-resolver.d.ts +0 -217
- package/dist/types/config/models-config-schema.d.ts +0 -700
- package/dist/types/config/prompt-templates.d.ts +0 -32
- package/dist/types/config/resolve-config-value.d.ts +0 -17
- package/dist/types/config/settings-schema.d.ts +0 -4223
- package/dist/types/config/settings.d.ts +0 -158
- package/dist/types/config/skill-settings-defaults.d.ts +0 -14
- package/dist/types/config/telegram-autostart.d.ts +0 -14
- package/dist/types/config/telegram-env-bridge.d.ts +0 -18
- package/dist/types/config.d.ts +0 -66
- package/dist/types/coordinator/contract.d.ts +0 -4
- package/dist/types/coordinator-mcp/model-preset.d.ts +0 -45
- package/dist/types/coordinator-mcp/policy.d.ts +0 -27
- package/dist/types/coordinator-mcp/safety.d.ts +0 -26
- package/dist/types/coordinator-mcp/server.d.ts +0 -139
- package/dist/types/coordinator-mcp/session-reaper.d.ts +0 -53
- package/dist/types/cursor.d.ts +0 -24
- package/dist/types/daemon/builtin.d.ts +0 -20
- package/dist/types/daemon/control-types.d.ts +0 -67
- package/dist/types/daemon/operator-contract.d.ts +0 -47
- package/dist/types/daemon/runtime.d.ts +0 -25
- package/dist/types/dap/client.d.ts +0 -39
- package/dist/types/dap/config.d.ts +0 -6
- package/dist/types/dap/index.d.ts +0 -4
- package/dist/types/dap/session.d.ts +0 -108
- package/dist/types/dap/types.d.ts +0 -524
- package/dist/types/debug/crash-diagnostics.d.ts +0 -45
- package/dist/types/debug/index.d.ts +0 -15
- package/dist/types/debug/log-formatting.d.ts +0 -4
- package/dist/types/debug/log-viewer.d.ts +0 -68
- package/dist/types/debug/profiler.d.ts +0 -24
- package/dist/types/debug/raw-sse-buffer.d.ts +0 -44
- package/dist/types/debug/raw-sse.d.ts +0 -16
- package/dist/types/debug/report-bundle.d.ts +0 -55
- package/dist/types/debug/runtime-gauges.d.ts +0 -6
- package/dist/types/debug/system-info.d.ts +0 -26
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +0 -11
- package/dist/types/deep-interview/render-middleware.d.ts +0 -6
- package/dist/types/defaults/skc/extensions/grok-build/index.d.ts +0 -1
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/index.d.ts +0 -1
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +0 -29
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +0 -27
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/billing.d.ts +0 -8
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/register.d.ts +0 -5
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/stream.d.ts +0 -10
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/provider/usage.d.ts +0 -2
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +0 -2
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +0 -38
- package/dist/types/defaults/skc-defaults.d.ts +0 -64
- package/dist/types/defaults/skc-grok-cli.d.ts +0 -5
- package/dist/types/discovery/agents-md.d.ts +0 -1
- package/dist/types/discovery/agents.d.ts +0 -12
- package/dist/types/discovery/builtin.d.ts +0 -1
- package/dist/types/discovery/claude-plugins.d.ts +0 -1
- package/dist/types/discovery/claude.d.ts +0 -1
- package/dist/types/discovery/cline.d.ts +0 -1
- package/dist/types/discovery/codex.d.ts +0 -1
- package/dist/types/discovery/cursor.d.ts +0 -19
- package/dist/types/discovery/gemini.d.ts +0 -1
- package/dist/types/discovery/github.d.ts +0 -1
- package/dist/types/discovery/helpers.d.ts +0 -271
- package/dist/types/discovery/index.d.ts +0 -48
- package/dist/types/discovery/mcp-json.d.ts +0 -1
- package/dist/types/discovery/opencode.d.ts +0 -1
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/discovery/ssh.d.ts +0 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -1
- package/dist/types/discovery/vscode.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +0 -15
- package/dist/types/edit/apply-patch/index.d.ts +0 -35
- package/dist/types/edit/apply-patch/parser.d.ts +0 -34
- package/dist/types/edit/diff.d.ts +0 -75
- package/dist/types/edit/file-read-cache.d.ts +0 -25
- package/dist/types/edit/index.d.ts +0 -58
- package/dist/types/edit/modes/apply-patch.d.ts +0 -24
- package/dist/types/edit/modes/patch.d.ts +0 -99
- package/dist/types/edit/modes/replace.d.ts +0 -149
- package/dist/types/edit/normalize.d.ts +0 -43
- package/dist/types/edit/notebook.d.ts +0 -26
- package/dist/types/edit/read-file.d.ts +0 -8
- package/dist/types/edit/renderer.d.ts +0 -110
- package/dist/types/edit/streaming.d.ts +0 -66
- package/dist/types/eval/backend.d.ts +0 -40
- package/dist/types/eval/index.d.ts +0 -4
- package/dist/types/eval/js/context-manager.d.ts +0 -27
- package/dist/types/eval/js/executor.d.ts +0 -29
- package/dist/types/eval/js/index.d.ts +0 -10
- package/dist/types/eval/js/shared/helpers.d.ts +0 -38
- package/dist/types/eval/js/shared/indirect-eval.d.ts +0 -14
- package/dist/types/eval/js/shared/prelude.d.ts +0 -1
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +0 -6
- package/dist/types/eval/js/shared/runtime.d.ts +0 -47
- package/dist/types/eval/js/shared/types.d.ts +0 -24
- package/dist/types/eval/js/tool-bridge.d.ts +0 -18
- package/dist/types/eval/js/worker-core.d.ts +0 -5
- package/dist/types/eval/js/worker-entry.d.ts +0 -1
- package/dist/types/eval/js/worker-protocol.d.ts +0 -77
- package/dist/types/eval/py/display.d.ts +0 -25
- package/dist/types/eval/py/executor.d.ts +0 -88
- package/dist/types/eval/py/index.d.ts +0 -10
- package/dist/types/eval/py/kernel.d.ts +0 -65
- package/dist/types/eval/py/prelude.d.ts +0 -1
- package/dist/types/eval/py/runtime.d.ts +0 -29
- package/dist/types/eval/py/tool-bridge.d.ts +0 -20
- package/dist/types/eval/types.d.ts +0 -52
- package/dist/types/exa/factory.d.ts +0 -13
- package/dist/types/exa/index.d.ts +0 -20
- package/dist/types/exa/mcp-client.d.ts +0 -45
- package/dist/types/exa/render.d.ts +0 -19
- package/dist/types/exa/researcher.d.ts +0 -9
- package/dist/types/exa/search.d.ts +0 -9
- package/dist/types/exa/types.d.ts +0 -155
- package/dist/types/exa/websets.d.ts +0 -9
- package/dist/types/exec/bash-executor.d.ts +0 -64
- package/dist/types/exec/exec.d.ts +0 -25
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/dist/types/exec/non-interactive-env.d.ts +0 -1
- package/dist/types/export/custom-share.d.ts +0 -20
- package/dist/types/export/html/index.d.ts +0 -10
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/export/html/template.macro.d.ts +0 -5
- package/dist/types/export/ttsr.d.ts +0 -55
- package/dist/types/extensibility/custom-commands/bundled/ci-green/index.d.ts +0 -9
- package/dist/types/extensibility/custom-commands/index.d.ts +0 -2
- package/dist/types/extensibility/custom-commands/loader.d.ts +0 -29
- package/dist/types/extensibility/custom-commands/types.d.ts +0 -106
- package/dist/types/extensibility/custom-tools/index.d.ts +0 -6
- package/dist/types/extensibility/custom-tools/loader.d.ts +0 -69
- package/dist/types/extensibility/custom-tools/types.d.ts +0 -229
- package/dist/types/extensibility/custom-tools/wrapper.d.ts +0 -24
- package/dist/types/extensibility/extensions/compact-handler.d.ts +0 -26
- package/dist/types/extensibility/extensions/get-commands-handler.d.ts +0 -29
- package/dist/types/extensibility/extensions/index.d.ts +0 -9
- package/dist/types/extensibility/extensions/loader.d.ts +0 -43
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -35
- package/dist/types/extensibility/extensions/runner.d.ts +0 -135
- package/dist/types/extensibility/extensions/types.d.ts +0 -920
- package/dist/types/extensibility/extensions/wrapper.d.ts +0 -55
- package/dist/types/extensibility/hooks/index.d.ts +0 -5
- package/dist/types/extensibility/hooks/loader.d.ts +0 -89
- package/dist/types/extensibility/hooks/runner.d.ts +0 -126
- package/dist/types/extensibility/hooks/tool-wrapper.d.ts +0 -25
- package/dist/types/extensibility/hooks/types.d.ts +0 -431
- package/dist/types/extensibility/plugins/doctor.d.ts +0 -3
- package/dist/types/extensibility/plugins/git-url.d.ts +0 -34
- package/dist/types/extensibility/plugins/index.d.ts +0 -8
- package/dist/types/extensibility/plugins/installer.d.ts +0 -5
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +0 -2
- package/dist/types/extensibility/plugins/loader.d.ts +0 -31
- package/dist/types/extensibility/plugins/manager.d.ts +0 -65
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +0 -41
- package/dist/types/extensibility/plugins/marketplace/fetcher.d.ts +0 -48
- package/dist/types/extensibility/plugins/marketplace/index.d.ts +0 -6
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +0 -57
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +0 -34
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +0 -28
- package/dist/types/extensibility/plugins/marketplace/types.d.ts +0 -130
- package/dist/types/extensibility/plugins/parser.d.ts +0 -52
- package/dist/types/extensibility/plugins/security-scanner.d.ts +0 -31
- package/dist/types/extensibility/plugins/types.d.ts +0 -149
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +0 -20
- package/dist/types/extensibility/shared-events.d.ts +0 -281
- package/dist/types/extensibility/skc-plugins/activation.d.ts +0 -14
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +0 -19
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +0 -29
- package/dist/types/extensibility/skc-plugins/index.d.ts +0 -18
- package/dist/types/extensibility/skc-plugins/injection.d.ts +0 -40
- package/dist/types/extensibility/skc-plugins/installer.d.ts +0 -13
- package/dist/types/extensibility/skc-plugins/loader.d.ts +0 -3
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +0 -26
- package/dist/types/extensibility/skc-plugins/observability.d.ts +0 -27
- package/dist/types/extensibility/skc-plugins/paths.d.ts +0 -8
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +0 -16
- package/dist/types/extensibility/skc-plugins/registry.d.ts +0 -32
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +0 -64
- package/dist/types/extensibility/skc-plugins/schema.d.ts +0 -3
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +0 -42
- package/dist/types/extensibility/skc-plugins/state.d.ts +0 -9
- package/dist/types/extensibility/skc-plugins/tools.d.ts +0 -8
- package/dist/types/extensibility/skc-plugins/types.d.ts +0 -220
- package/dist/types/extensibility/skc-plugins/validation.d.ts +0 -11
- package/dist/types/extensibility/skills.d.ts +0 -82
- package/dist/types/extensibility/slash-commands.d.ts +0 -48
- package/dist/types/extensibility/tool-proxy.d.ts +0 -4
- package/dist/types/extensibility/typebox.d.ts +0 -155
- package/dist/types/extensibility/utils.d.ts +0 -12
- package/dist/types/goals/index.d.ts +0 -3
- package/dist/types/goals/runtime.d.ts +0 -63
- package/dist/types/goals/state.d.ts +0 -32
- package/dist/types/goals/tools/goal-tool.d.ts +0 -62
- package/dist/types/harness-control-plane/classifier.d.ts +0 -13
- package/dist/types/harness-control-plane/control-endpoint.d.ts +0 -31
- package/dist/types/harness-control-plane/finalize.d.ts +0 -60
- package/dist/types/harness-control-plane/gc-adapter.d.ts +0 -3
- package/dist/types/harness-control-plane/operate.d.ts +0 -35
- package/dist/types/harness-control-plane/owner.d.ts +0 -53
- package/dist/types/harness-control-plane/phase-rollup.d.ts +0 -23
- package/dist/types/harness-control-plane/preserve.d.ts +0 -19
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +0 -19
- package/dist/types/harness-control-plane/receipt-spool.d.ts +0 -19
- package/dist/types/harness-control-plane/receipts.d.ts +0 -149
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
- package/dist/types/harness-control-plane/seams.d.ts +0 -21
- package/dist/types/harness-control-plane/session-lease.d.ts +0 -65
- package/dist/types/harness-control-plane/state-machine.d.ts +0 -24
- package/dist/types/harness-control-plane/storage.d.ts +0 -81
- package/dist/types/harness-control-plane/types.d.ts +0 -187
- package/dist/types/hashline/anchors.d.ts +0 -20
- package/dist/types/hashline/apply.d.ts +0 -14
- package/dist/types/hashline/constants.d.ts +0 -19
- package/dist/types/hashline/diff-preview.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +0 -17
- package/dist/types/hashline/execute.d.ts +0 -4
- package/dist/types/hashline/hash.d.ts +0 -123
- package/dist/types/hashline/index.d.ts +0 -13
- package/dist/types/hashline/input.d.ts +0 -11
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/hashline/prefixes.d.ts +0 -7
- package/dist/types/hashline/recovery.d.ts +0 -27
- package/dist/types/hashline/stream.d.ts +0 -2
- package/dist/types/hashline/types.d.ts +0 -77
- package/dist/types/hindsight/backend.d.ts +0 -13
- package/dist/types/hindsight/bank.d.ts +0 -54
- package/dist/types/hindsight/client.d.ts +0 -224
- package/dist/types/hindsight/config.d.ts +0 -51
- package/dist/types/hindsight/content.d.ts +0 -70
- package/dist/types/hindsight/index.d.ts +0 -8
- package/dist/types/hindsight/mental-models.d.ts +0 -125
- package/dist/types/hindsight/state.d.ts +0 -99
- package/dist/types/hindsight/transcript.d.ts +0 -28
- package/dist/types/hooks/codex-native-hooks-config.d.ts +0 -29
- package/dist/types/hooks/native-skill-hook.d.ts +0 -28
- package/dist/types/hooks/skill-keywords.d.ts +0 -18
- package/dist/types/hooks/skill-state.d.ts +0 -93
- package/dist/types/i18n/index.d.ts +0 -38
- package/dist/types/i18n/messages/de.d.ts +0 -2
- package/dist/types/i18n/messages/de.messages.d.ts +0 -1
- package/dist/types/i18n/messages/de.settings.d.ts +0 -1
- package/dist/types/i18n/messages/en.d.ts +0 -81
- package/dist/types/i18n/messages/es.d.ts +0 -2
- package/dist/types/i18n/messages/es.messages.d.ts +0 -1
- package/dist/types/i18n/messages/es.settings.d.ts +0 -1
- package/dist/types/i18n/messages/fr.d.ts +0 -2
- package/dist/types/i18n/messages/fr.messages.d.ts +0 -1
- package/dist/types/i18n/messages/fr.settings.d.ts +0 -1
- package/dist/types/i18n/messages/ja.d.ts +0 -2
- package/dist/types/i18n/messages/ja.messages.d.ts +0 -1
- package/dist/types/i18n/messages/ja.settings.d.ts +0 -1
- package/dist/types/i18n/messages/ko.d.ts +0 -2
- package/dist/types/i18n/messages/ko.messages.d.ts +0 -1
- package/dist/types/i18n/messages/ko.settings.d.ts +0 -1
- package/dist/types/i18n/messages/zh.d.ts +0 -2
- package/dist/types/i18n/messages/zh.messages.d.ts +0 -1
- package/dist/types/i18n/messages/zh.settings.d.ts +0 -1
- package/dist/types/index.d.ts +0 -33
- package/dist/types/internal-urls/agent-protocol.d.ts +0 -12
- package/dist/types/internal-urls/artifact-protocol.d.ts +0 -6
- package/dist/types/internal-urls/docs-index.generated.d.ts +0 -2
- package/dist/types/internal-urls/index.d.ts +0 -20
- package/dist/types/internal-urls/issue-pr-protocol.d.ts +0 -17
- package/dist/types/internal-urls/json-query.d.ts +0 -30
- package/dist/types/internal-urls/local-protocol.d.ts +0 -43
- package/dist/types/internal-urls/mcp-protocol.d.ts +0 -12
- package/dist/types/internal-urls/memory-protocol.d.ts +0 -23
- package/dist/types/internal-urls/parse.d.ts +0 -19
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -11
- package/dist/types/internal-urls/router.d.ts +0 -14
- package/dist/types/internal-urls/rule-protocol.d.ts +0 -6
- package/dist/types/internal-urls/skc-protocol.d.ts +0 -12
- package/dist/types/internal-urls/skill-protocol.d.ts +0 -13
- package/dist/types/internal-urls/types.d.ts +0 -109
- package/dist/types/lsp/client.d.ts +0 -66
- package/dist/types/lsp/clients/biome-client.d.ts +0 -16
- package/dist/types/lsp/clients/index.d.ts +0 -19
- package/dist/types/lsp/clients/lsp-linter-client.d.ts +0 -16
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -20
- package/dist/types/lsp/config.d.ts +0 -65
- package/dist/types/lsp/edits.d.ts +0 -23
- package/dist/types/lsp/index.d.ts +0 -130
- package/dist/types/lsp/lspmux.d.ts +0 -58
- package/dist/types/lsp/render.d.ts +0 -32
- package/dist/types/lsp/startup-events.d.ts +0 -12
- package/dist/types/lsp/types.d.ts +0 -306
- package/dist/types/lsp/utils.d.ts +0 -108
- package/dist/types/main.d.ts +0 -139
- package/dist/types/memories/index.d.ts +0 -28
- package/dist/types/memories/storage.d.ts +0 -110
- package/dist/types/memory-backend/index.d.ts +0 -4
- package/dist/types/memory-backend/local-backend.d.ts +0 -9
- package/dist/types/memory-backend/off-backend.d.ts +0 -7
- package/dist/types/memory-backend/resolve.d.ts +0 -15
- package/dist/types/memory-backend/types.d.ts +0 -61
- package/dist/types/migrate/action-planner.d.ts +0 -11
- package/dist/types/migrate/adapters/claude-code.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +0 -5
- package/dist/types/migrate/adapters/index.d.ts +0 -45
- package/dist/types/migrate/adapters/opencode.d.ts +0 -2
- package/dist/types/migrate/executor.d.ts +0 -2
- package/dist/types/migrate/mcp-mapper.d.ts +0 -20
- package/dist/types/migrate/report.d.ts +0 -18
- package/dist/types/migrate/skill-normalizer.d.ts +0 -27
- package/dist/types/migrate/types.d.ts +0 -126
- package/dist/types/modes/acp/acp-agent.d.ts +0 -78
- package/dist/types/modes/acp/acp-client-bridge.d.ts +0 -9
- package/dist/types/modes/acp/acp-event-mapper.d.ts +0 -36
- package/dist/types/modes/acp/acp-mode.d.ts +0 -17
- package/dist/types/modes/acp/index.d.ts +0 -2
- package/dist/types/modes/acp/terminal-auth.d.ts +0 -6
- package/dist/types/modes/bridge/auth.d.ts +0 -12
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
- package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
- package/dist/types/modes/bridge/event-stream.d.ts +0 -8
- package/dist/types/modes/components/agent-dashboard.d.ts +0 -21
- package/dist/types/modes/components/assistant-message.d.ts +0 -20
- package/dist/types/modes/components/bash-execution.d.ts +0 -29
- package/dist/types/modes/components/bordered-loader.d.ts +0 -11
- package/dist/types/modes/components/branch-summary-message.d.ts +0 -13
- package/dist/types/modes/components/btw-panel.d.ts +0 -16
- package/dist/types/modes/components/compaction-summary-message.d.ts +0 -13
- package/dist/types/modes/components/countdown-timer.d.ts +0 -14
- package/dist/types/modes/components/custom-editor.d.ts +0 -69
- package/dist/types/modes/components/custom-message.d.ts +0 -15
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +0 -12
- package/dist/types/modes/components/custom-provider-wizard.d.ts +0 -10
- package/dist/types/modes/components/diff.d.ts +0 -11
- package/dist/types/modes/components/dynamic-border.d.ts +0 -14
- package/dist/types/modes/components/eval-execution.d.ts +0 -23
- package/dist/types/modes/components/execution-shared.d.ts +0 -46
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +0 -26
- package/dist/types/modes/components/extensions/extension-list.d.ts +0 -39
- package/dist/types/modes/components/extensions/index.d.ts +0 -8
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +0 -8
- package/dist/types/modes/components/extensions/state-manager.d.ts +0 -50
- package/dist/types/modes/components/extensions/types.d.ts +0 -151
- package/dist/types/modes/components/footer.d.ts +0 -30
- package/dist/types/modes/components/history-search.d.ts +0 -7
- package/dist/types/modes/components/hook-editor.d.ts +0 -18
- package/dist/types/modes/components/hook-input.d.ts +0 -15
- package/dist/types/modes/components/hook-message.d.ts +0 -15
- package/dist/types/modes/components/hook-selector.d.ts +0 -59
- package/dist/types/modes/components/index.d.ts +0 -35
- package/dist/types/modes/components/irc-sidebar.d.ts +0 -32
- package/dist/types/modes/components/jobs-overlay-model.d.ts +0 -31
- package/dist/types/modes/components/jobs-overlay.d.ts +0 -30
- package/dist/types/modes/components/keybinding-hints.d.ts +0 -40
- package/dist/types/modes/components/login-dialog.d.ts +0 -32
- package/dist/types/modes/components/message-frame.d.ts +0 -42
- package/dist/types/modes/components/model-selector.d.ts +0 -60
- package/dist/types/modes/components/oauth-selector.d.ts +0 -17
- package/dist/types/modes/components/pet-selector.d.ts +0 -11
- package/dist/types/modes/components/plugin-selector.d.ts +0 -26
- package/dist/types/modes/components/plugin-settings.d.ts +0 -66
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +0 -7
- package/dist/types/modes/components/queue-mode-selector.d.ts +0 -9
- package/dist/types/modes/components/queued-message-selector.d.ts +0 -12
- package/dist/types/modes/components/read-tool-group.d.ts +0 -30
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +0 -26
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +0 -59
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/modes/components/session-selector.d.ts +0 -43
- package/dist/types/modes/components/settings-defs.d.ts +0 -50
- package/dist/types/modes/components/settings-selector.d.ts +0 -64
- package/dist/types/modes/components/show-images-selector.d.ts +0 -9
- package/dist/types/modes/components/skill-hud/render.d.ts +0 -2
- package/dist/types/modes/components/skill-message.d.ts +0 -10
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -4
- package/dist/types/modes/components/status-line/git-utils.d.ts +0 -28
- package/dist/types/modes/components/status-line/index.d.ts +0 -4
- package/dist/types/modes/components/status-line/presets.d.ts +0 -3
- package/dist/types/modes/components/status-line/segments.d.ts +0 -5
- package/dist/types/modes/components/status-line/separators.d.ts +0 -3
- package/dist/types/modes/components/status-line/token-rate.d.ts +0 -10
- package/dist/types/modes/components/status-line/types.d.ts +0 -83
- package/dist/types/modes/components/status-line.d.ts +0 -94
- package/dist/types/modes/components/theme-selector.d.ts +0 -10
- package/dist/types/modes/components/thinking-selector.d.ts +0 -15
- package/dist/types/modes/components/todo-reminder.d.ts +0 -13
- package/dist/types/modes/components/tool-execution.d.ts +0 -55
- package/dist/types/modes/components/tree-selector.d.ts +0 -31
- package/dist/types/modes/components/ttsr-notification.d.ts +0 -13
- package/dist/types/modes/components/two-column-body.d.ts +0 -9
- package/dist/types/modes/components/user-message-selector.d.ts +0 -28
- package/dist/types/modes/components/user-message.d.ts +0 -8
- package/dist/types/modes/components/visual-truncate.d.ts +0 -19
- package/dist/types/modes/components/welcome.d.ts +0 -46
- package/dist/types/modes/controllers/btw-controller.d.ts +0 -10
- package/dist/types/modes/controllers/command-controller-shared.d.ts +0 -47
- package/dist/types/modes/controllers/command-controller.d.ts +0 -40
- package/dist/types/modes/controllers/event-controller.d.ts +0 -21
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -80
- package/dist/types/modes/controllers/input-controller.d.ts +0 -42
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +0 -10
- package/dist/types/modes/controllers/selector-controller.d.ts +0 -68
- package/dist/types/modes/controllers/ssh-command-controller.d.ts +0 -10
- package/dist/types/modes/controllers/todo-command-controller.d.ts +0 -7
- package/dist/types/modes/emoji-autocomplete.d.ts +0 -16
- package/dist/types/modes/index.d.ts +0 -10
- package/dist/types/modes/interactive-mode.d.ts +0 -297
- package/dist/types/modes/irc-observation-ledger.d.ts +0 -22
- package/dist/types/modes/jobs-observer.d.ts +0 -57
- package/dist/types/modes/oauth-manual-input.d.ts +0 -8
- package/dist/types/modes/print-mode.d.ts +0 -44
- package/dist/types/modes/prompt-action-autocomplete.d.ts +0 -50
- package/dist/types/modes/rpc/host-tools.d.ts +0 -1
- package/dist/types/modes/rpc/host-uris.d.ts +0 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
- package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
- package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
- package/dist/types/modes/runtime-init.d.ts +0 -21
- package/dist/types/modes/session-observer-registry.d.ts +0 -26
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +0 -57
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +0 -102
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +0 -103
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +0 -60
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +0 -37
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +0 -154
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +0 -39
- package/dist/types/modes/shared.d.ts +0 -15
- package/dist/types/modes/slash-command-visibility.d.ts +0 -1
- package/dist/types/modes/theme/defaults/index.d.ts +0 -652
- package/dist/types/modes/theme/mermaid-cache.d.ts +0 -9
- package/dist/types/modes/theme/shimmer.d.ts +0 -38
- package/dist/types/modes/theme/theme.d.ts +0 -275
- package/dist/types/modes/tmux-scroll.d.ts +0 -18
- package/dist/types/modes/types.d.ts +0 -318
- package/dist/types/modes/utils/abort-message.d.ts +0 -4
- package/dist/types/modes/utils/context-usage.d.ts +0 -36
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +0 -5
- package/dist/types/modes/utils/injected-user-submission.d.ts +0 -52
- package/dist/types/modes/utils/irc-message.d.ts +0 -38
- package/dist/types/modes/utils/keybinding-matchers.d.ts +0 -10
- package/dist/types/modes/utils/terminal-bell.d.ts +0 -3
- package/dist/types/modes/utils/tools-markdown.d.ts +0 -5
- package/dist/types/modes/utils/ui-helpers.d.ts +0 -75
- package/dist/types/notifications/attachment-registry.d.ts +0 -17
- package/dist/types/notifications/chat-adapters.d.ts +0 -9
- package/dist/types/notifications/config-commands.d.ts +0 -64
- package/dist/types/notifications/config.d.ts +0 -132
- package/dist/types/notifications/daemon-paths.d.ts +0 -17
- package/dist/types/notifications/engine.d.ts +0 -59
- package/dist/types/notifications/helpers.d.ts +0 -55
- package/dist/types/notifications/html-format.d.ts +0 -75
- package/dist/types/notifications/index.d.ts +0 -219
- package/dist/types/notifications/lifecycle-commands.d.ts +0 -69
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +0 -107
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +0 -144
- package/dist/types/notifications/managed-daemon.d.ts +0 -48
- package/dist/types/notifications/notification-service.d.ts +0 -144
- package/dist/types/notifications/operator-runtime.d.ts +0 -52
- package/dist/types/notifications/rate-limit-pool.d.ts +0 -113
- package/dist/types/notifications/recent-activity.d.ts +0 -39
- package/dist/types/notifications/reply-sent-store.d.ts +0 -53
- package/dist/types/notifications/rich-draft.d.ts +0 -68
- package/dist/types/notifications/rich-render.d.ts +0 -90
- package/dist/types/notifications/telegram-cli.d.ts +0 -19
- package/dist/types/notifications/telegram-daemon-cli.d.ts +0 -28
- package/dist/types/notifications/telegram-daemon-control.d.ts +0 -70
- package/dist/types/notifications/telegram-daemon.d.ts +0 -558
- package/dist/types/notifications/telegram-reference.d.ts +0 -136
- package/dist/types/notifications/threaded-inbound.d.ts +0 -77
- package/dist/types/notifications/threaded-render.d.ts +0 -88
- package/dist/types/notifications/topic-registry.d.ts +0 -89
- package/dist/types/plan-mode/approved-plan.d.ts +0 -49
- package/dist/types/plan-mode/state.d.ts +0 -6
- package/dist/types/registry/agent-registry.d.ts +0 -64
- package/dist/types/reminders/star-reminder.d.ts +0 -115
- package/dist/types/research-plan/index.d.ts +0 -1
- package/dist/types/research-plan/ledger.d.ts +0 -33
- package/dist/types/rlm/artifacts.d.ts +0 -9
- package/dist/types/rlm/complete-research-tool.d.ts +0 -35
- package/dist/types/rlm/data-context.d.ts +0 -6
- package/dist/types/rlm/index.d.ts +0 -47
- package/dist/types/rlm/notebook.d.ts +0 -12
- package/dist/types/rlm/preset.d.ts +0 -23
- package/dist/types/rlm/python-tool.d.ts +0 -16
- package/dist/types/rlm/report.d.ts +0 -14
- package/dist/types/rlm/types.d.ts +0 -37
- package/dist/types/runtime/process-lifecycle.d.ts +0 -108
- package/dist/types/runtime-api-key.d.ts +0 -7
- package/dist/types/runtime-credential-selector.d.ts +0 -7
- package/dist/types/runtime-mcp/client.d.ts +0 -74
- package/dist/types/runtime-mcp/config-writer.d.ts +0 -89
- package/dist/types/runtime-mcp/config.d.ts +0 -77
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/runtime-mcp/index.d.ts +0 -18
- package/dist/types/runtime-mcp/json-rpc.d.ts +0 -22
- package/dist/types/runtime-mcp/loader.d.ts +0 -45
- package/dist/types/runtime-mcp/manager.d.ts +0 -195
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +0 -40
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -59
- package/dist/types/runtime-mcp/render.d.ts +0 -25
- package/dist/types/runtime-mcp/smithery-auth.d.ts +0 -16
- package/dist/types/runtime-mcp/smithery-connect.d.ts +0 -38
- package/dist/types/runtime-mcp/smithery-registry.d.ts +0 -51
- package/dist/types/runtime-mcp/tool-bridge.d.ts +0 -86
- package/dist/types/runtime-mcp/tool-cache.d.ts +0 -8
- package/dist/types/runtime-mcp/transports/http.d.ts +0 -36
- package/dist/types/runtime-mcp/transports/index.d.ts +0 -5
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -29
- package/dist/types/runtime-mcp/types.d.ts +0 -349
- package/dist/types/sdk.d.ts +0 -256
- package/dist/types/secrets/index.d.ts +0 -9
- package/dist/types/secrets/obfuscator.d.ts +0 -23
- package/dist/types/secrets/regex.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +0 -1238
- package/dist/types/session/agent-storage.d.ts +0 -101
- package/dist/types/session/artifacts.d.ts +0 -64
- package/dist/types/session/auth-broker-config.d.ts +0 -13
- package/dist/types/session/auth-storage.d.ts +0 -6
- package/dist/types/session/blob-store.d.ts +0 -160
- package/dist/types/session/cache-economics.d.ts +0 -79
- package/dist/types/session/client-bridge.d.ts +0 -88
- package/dist/types/session/context-estimation.d.ts +0 -31
- package/dist/types/session/contribution-prep.d.ts +0 -47
- package/dist/types/session/file-mention-pruning.d.ts +0 -8
- package/dist/types/session/history-storage.d.ts +0 -16
- package/dist/types/session/messages.d.ts +0 -169
- package/dist/types/session/provider-safety-stop.d.ts +0 -5
- package/dist/types/session/session-dump-format.d.ts +0 -22
- package/dist/types/session/session-manager.d.ts +0 -781
- package/dist/types/session/session-storage.d.ts +0 -230
- package/dist/types/session/streaming-output.d.ts +0 -209
- package/dist/types/session/tool-choice-queue.d.ts +0 -84
- package/dist/types/session/yield-queue.d.ts +0 -24
- package/dist/types/setup/credential-auto-import.d.ts +0 -63
- package/dist/types/setup/credential-import.d.ts +0 -82
- package/dist/types/setup/hermes-setup.d.ts +0 -78
- package/dist/types/setup/host-plugin-setup.d.ts +0 -39
- package/dist/types/setup/model-onboarding-guidance.d.ts +0 -9
- package/dist/types/setup/provider-onboarding.d.ts +0 -52
- package/dist/types/skc-runtime/cli-write-receipt.d.ts +0 -24
- package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +0 -39
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +0 -114
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +0 -36
- package/dist/types/skc-runtime/deep-interview-state.d.ts +0 -127
- package/dist/types/skc-runtime/gc-render.d.ts +0 -6
- package/dist/types/skc-runtime/gc-runtime.d.ts +0 -134
- package/dist/types/skc-runtime/goal-mode-request.d.ts +0 -53
- package/dist/types/skc-runtime/launch-tmux.d.ts +0 -78
- package/dist/types/skc-runtime/launch-worktree.d.ts +0 -46
- package/dist/types/skc-runtime/ledger-event-renderer.d.ts +0 -68
- package/dist/types/skc-runtime/psmux-detect.d.ts +0 -78
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +0 -26
- package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +0 -3
- package/dist/types/skc-runtime/session-layout.d.ts +0 -59
- package/dist/types/skc-runtime/session-resolution.d.ts +0 -47
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +0 -70
- package/dist/types/skc-runtime/state-graph.d.ts +0 -4
- package/dist/types/skc-runtime/state-migrations.d.ts +0 -33
- package/dist/types/skc-runtime/state-renderer.d.ts +0 -70
- package/dist/types/skc-runtime/state-runtime.d.ts +0 -38
- package/dist/types/skc-runtime/state-schema.d.ts +0 -319
- package/dist/types/skc-runtime/state-validation.d.ts +0 -6
- package/dist/types/skc-runtime/state-writer.d.ts +0 -244
- package/dist/types/skc-runtime/team-gc.d.ts +0 -7
- package/dist/types/skc-runtime/team-runtime.d.ts +0 -355
- package/dist/types/skc-runtime/tmux-common.d.ts +0 -111
- package/dist/types/skc-runtime/tmux-gc.d.ts +0 -7
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +0 -5
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +0 -309
- package/dist/types/skc-runtime/tmux-sessions.d.ts +0 -94
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +0 -93
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +0 -42
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -410
- package/dist/types/skc-runtime/workflow-command-ref.d.ts +0 -43
- package/dist/types/skc-runtime/workflow-manifest.d.ts +0 -54
- package/dist/types/skill-state/active-state.d.ts +0 -126
- package/dist/types/skill-state/canonical-skills.d.ts +0 -3
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +0 -38
- package/dist/types/skill-state/initial-phase.d.ts +0 -12
- package/dist/types/skill-state/workflow-hud.d.ts +0 -83
- package/dist/types/skill-state/workflow-state-contract.d.ts +0 -57
- package/dist/types/skill-state/workflow-state-version.d.ts +0 -3
- package/dist/types/slash-commands/acp-builtins.d.ts +0 -18
- package/dist/types/slash-commands/builtin-registry.d.ts +0 -24
- package/dist/types/slash-commands/helpers/context-report.d.ts +0 -7
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +0 -82
- package/dist/types/slash-commands/helpers/format.d.ts +0 -10
- package/dist/types/slash-commands/helpers/mcp.d.ts +0 -3
- package/dist/types/slash-commands/helpers/parse.d.ts +0 -33
- package/dist/types/slash-commands/helpers/ssh.d.ts +0 -3
- package/dist/types/slash-commands/helpers/todo.d.ts +0 -3
- package/dist/types/slash-commands/helpers/usage-report.d.ts +0 -7
- package/dist/types/slash-commands/types.d.ts +0 -120
- package/dist/types/ssh/config-writer.d.ts +0 -49
- package/dist/types/ssh/connection-manager.d.ts +0 -33
- package/dist/types/ssh/ssh-executor.d.ts +0 -37
- package/dist/types/ssh/sshfs-mount.d.ts +0 -6
- package/dist/types/ssh/utils.d.ts +0 -2
- package/dist/types/stt/downloader.d.ts +0 -9
- package/dist/types/stt/index.d.ts +0 -3
- package/dist/types/stt/recorder.d.ts +0 -13
- package/dist/types/stt/setup.d.ts +0 -18
- package/dist/types/stt/stt-controller.d.ts +0 -16
- package/dist/types/stt/transcriber.d.ts +0 -16
- package/dist/types/system-prompt.d.ts +0 -118
- package/dist/types/task/agents.d.ts +0 -28
- package/dist/types/task/commands.d.ts +0 -35
- package/dist/types/task/discovery.d.ts +0 -19
- package/dist/types/task/executor.d.ts +0 -127
- package/dist/types/task/fork-context-advisory.d.ts +0 -13
- package/dist/types/task/fork-context-budget.d.ts +0 -7
- package/dist/types/task/id.d.ts +0 -7
- package/dist/types/task/index.d.ts +0 -43
- package/dist/types/task/name-generator.d.ts +0 -16
- package/dist/types/task/output-manager.d.ts +0 -36
- package/dist/types/task/parallel.d.ts +0 -34
- package/dist/types/task/receipt.d.ts +0 -89
- package/dist/types/task/render.d.ts +0 -28
- package/dist/types/task/roi-reconciliation.d.ts +0 -27
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/dist/types/task/skc-command.d.ts +0 -7
- package/dist/types/task/spawn-gate.d.ts +0 -29
- package/dist/types/task/subprocess-tool-registry.d.ts +0 -72
- package/dist/types/task/token-log.d.ts +0 -30
- package/dist/types/task/token-log.test.d.ts +0 -1
- package/dist/types/task/types.d.ts +0 -516
- package/dist/types/task/worktree.d.ts +0 -94
- package/dist/types/thinking-metadata.d.ts +0 -16
- package/dist/types/thinking.d.ts +0 -22
- package/dist/types/tool-discovery/tool-index.d.ts +0 -112
- package/dist/types/tools/archive-reader.d.ts +0 -40
- package/dist/types/tools/ask-answer-registry.d.ts +0 -18
- package/dist/types/tools/ask.d.ts +0 -123
- package/dist/types/tools/ast-edit.d.ts +0 -77
- package/dist/types/tools/ast-grep.d.ts +0 -69
- package/dist/types/tools/auto-generated-guard.d.ts +0 -17
- package/dist/types/tools/bash-allowed-prefixes.d.ts +0 -10
- package/dist/types/tools/bash-command-fixup.d.ts +0 -11
- package/dist/types/tools/bash-interactive.d.ts +0 -16
- package/dist/types/tools/bash-interceptor.d.ts +0 -24
- package/dist/types/tools/bash-pty-selection.d.ts +0 -7
- package/dist/types/tools/bash-skill-urls.d.ts +0 -31
- package/dist/types/tools/bash.d.ts +0 -157
- package/dist/types/tools/bisect.d.ts +0 -81
- package/dist/types/tools/browser/actions.d.ts +0 -54
- package/dist/types/tools/browser/attach.d.ts +0 -50
- package/dist/types/tools/browser/launch.d.ts +0 -62
- package/dist/types/tools/browser/readable.d.ts +0 -17
- package/dist/types/tools/browser/registry.d.ts +0 -57
- package/dist/types/tools/browser/render.d.ts +0 -53
- package/dist/types/tools/browser/tab-protocol.d.ts +0 -144
- package/dist/types/tools/browser/tab-supervisor.d.ts +0 -109
- package/dist/types/tools/browser/tab-worker-entry.d.ts +0 -1
- package/dist/types/tools/browser/tab-worker.d.ts +0 -19
- package/dist/types/tools/browser.d.ts +0 -211
- package/dist/types/tools/calculator.d.ts +0 -76
- package/dist/types/tools/checkpoint.d.ts +0 -63
- package/dist/types/tools/composer-bash-policy.d.ts +0 -14
- package/dist/types/tools/computer/render.d.ts +0 -17
- package/dist/types/tools/computer-gc.d.ts +0 -23
- package/dist/types/tools/computer.d.ts +0 -465
- package/dist/types/tools/conflict-detect.d.ts +0 -205
- package/dist/types/tools/context.d.ts +0 -19
- package/dist/types/tools/cron.d.ts +0 -91
- package/dist/types/tools/debug.d.ts +0 -209
- package/dist/types/tools/eval.d.ts +0 -108
- package/dist/types/tools/fetch.d.ts +0 -84
- package/dist/types/tools/file-recorder.d.ts +0 -13
- package/dist/types/tools/find.d.ts +0 -95
- package/dist/types/tools/fs-cache-invalidation.d.ts +0 -15
- package/dist/types/tools/gh-format.d.ts +0 -6
- package/dist/types/tools/gh-renderer.d.ts +0 -25
- package/dist/types/tools/gh.d.ts +0 -356
- package/dist/types/tools/github-cache.d.ts +0 -103
- package/dist/types/tools/grouped-file-output.d.ts +0 -36
- package/dist/types/tools/hindsight-recall.d.ts +0 -21
- package/dist/types/tools/hindsight-reflect.d.ts +0 -23
- package/dist/types/tools/hindsight-retain.d.ts +0 -27
- package/dist/types/tools/image-gen.d.ts +0 -79
- package/dist/types/tools/index.d.ts +0 -371
- package/dist/types/tools/irc.d.ts +0 -79
- package/dist/types/tools/job.d.ts +0 -78
- package/dist/types/tools/json-tree.d.ts +0 -23
- package/dist/types/tools/jtd-to-json-schema.d.ts +0 -29
- package/dist/types/tools/jtd-to-typescript.d.ts +0 -26
- package/dist/types/tools/jtd-utils.d.ts +0 -42
- package/dist/types/tools/list-limit.d.ts +0 -12
- package/dist/types/tools/match-line-format.d.ts +0 -11
- package/dist/types/tools/monitor.d.ts +0 -54
- package/dist/types/tools/output-meta.d.ts +0 -204
- package/dist/types/tools/path-utils.d.ts +0 -147
- package/dist/types/tools/plan-mode-guard.d.ts +0 -18
- package/dist/types/tools/read.d.ts +0 -85
- package/dist/types/tools/recipe/index.d.ts +0 -45
- package/dist/types/tools/recipe/render.d.ts +0 -36
- package/dist/types/tools/recipe/runner.d.ts +0 -60
- package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
- package/dist/types/tools/recipe/runners/index.d.ts +0 -2
- package/dist/types/tools/recipe/runners/just.d.ts +0 -2
- package/dist/types/tools/recipe/runners/make.d.ts +0 -2
- package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
- package/dist/types/tools/recipe/runners/task.d.ts +0 -2
- package/dist/types/tools/render-mermaid.d.ts +0 -37
- package/dist/types/tools/render-utils.d.ts +0 -158
- package/dist/types/tools/renderers.d.ts +0 -26
- package/dist/types/tools/resolve.d.ts +0 -80
- package/dist/types/tools/resource-gc.d.ts +0 -54
- package/dist/types/tools/review.d.ts +0 -63
- package/dist/types/tools/search-tool-bm25.d.ts +0 -65
- package/dist/types/tools/search.d.ts +0 -93
- package/dist/types/tools/skill-discovery.d.ts +0 -40
- package/dist/types/tools/skill.d.ts +0 -48
- package/dist/types/tools/sqlite-reader.d.ts +0 -94
- package/dist/types/tools/ssh.d.ts +0 -67
- package/dist/types/tools/subagent-render.d.ts +0 -31
- package/dist/types/tools/subagent.d.ts +0 -117
- package/dist/types/tools/telegram-send.d.ts +0 -39
- package/dist/types/tools/todo-write.d.ts +0 -120
- package/dist/types/tools/tool-errors.d.ts +0 -33
- package/dist/types/tools/tool-result.d.ts +0 -30
- package/dist/types/tools/tool-timeouts.d.ts +0 -56
- package/dist/types/tools/ultragoal-ask-guard.d.ts +0 -12
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/tools/write.d.ts +0 -59
- package/dist/types/tools/yield.d.ts +0 -25
- package/dist/types/tui/code-cell.d.ts +0 -32
- package/dist/types/tui/file-list.d.ts +0 -22
- package/dist/types/tui/hyperlink.d.ts +0 -42
- package/dist/types/tui/index.d.ts +0 -11
- package/dist/types/tui/output-block.d.ts +0 -28
- package/dist/types/tui/status-line.d.ts +0 -18
- package/dist/types/tui/tree-list.d.ts +0 -19
- package/dist/types/tui/types.d.ts +0 -13
- package/dist/types/tui/utils.d.ts +0 -36
- package/dist/types/utils/changelog.d.ts +0 -37
- package/dist/types/utils/clipboard.d.ts +0 -22
- package/dist/types/utils/cmux-workspace.d.ts +0 -52
- package/dist/types/utils/command-args.d.ts +0 -9
- package/dist/types/utils/commit-message-generator.d.ts +0 -7
- package/dist/types/utils/edit-mode.d.ts +0 -13
- package/dist/types/utils/event-bus.d.ts +0 -6
- package/dist/types/utils/external-editor.d.ts +0 -19
- package/dist/types/utils/file-display-mode.d.ts +0 -27
- package/dist/types/utils/file-mentions.d.ts +0 -25
- package/dist/types/utils/git.d.ts +0 -393
- package/dist/types/utils/image-loading.d.ts +0 -26
- package/dist/types/utils/image-resize.d.ts +0 -39
- package/dist/types/utils/lang-from-path.d.ts +0 -8
- package/dist/types/utils/linkedom.d.ts +0 -4
- package/dist/types/utils/markit.d.ts +0 -7
- package/dist/types/utils/open.d.ts +0 -2
- package/dist/types/utils/pasted-image-path.d.ts +0 -32
- package/dist/types/utils/session-color.d.ts +0 -10
- package/dist/types/utils/shell-snapshot.d.ts +0 -5
- package/dist/types/utils/sixel.d.ts +0 -21
- package/dist/types/utils/title-generator.d.ts +0 -31
- package/dist/types/utils/tool-choice.d.ts +0 -20
- package/dist/types/utils/tools-manager.d.ts +0 -9
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/dist/types/web/insane/bridge.d.ts +0 -103
- package/dist/types/web/insane/url-guard.d.ts +0 -25
- package/dist/types/web/kagi.d.ts +0 -23
- package/dist/types/web/parallel.d.ts +0 -58
- package/dist/types/web/scrapers/artifacthub.d.ts +0 -6
- package/dist/types/web/scrapers/arxiv.d.ts +0 -5
- package/dist/types/web/scrapers/aur.d.ts +0 -5
- package/dist/types/web/scrapers/biorxiv.d.ts +0 -5
- package/dist/types/web/scrapers/bluesky.d.ts +0 -5
- package/dist/types/web/scrapers/brew.d.ts +0 -5
- package/dist/types/web/scrapers/cheatsh.d.ts +0 -8
- package/dist/types/web/scrapers/chocolatey.d.ts +0 -5
- package/dist/types/web/scrapers/choosealicense.d.ts +0 -2
- package/dist/types/web/scrapers/cisa-kev.d.ts +0 -5
- package/dist/types/web/scrapers/clojars.d.ts +0 -5
- package/dist/types/web/scrapers/coingecko.d.ts +0 -5
- package/dist/types/web/scrapers/crates-io.d.ts +0 -5
- package/dist/types/web/scrapers/crossref.d.ts +0 -2
- package/dist/types/web/scrapers/devto.d.ts +0 -5
- package/dist/types/web/scrapers/discogs.d.ts +0 -8
- package/dist/types/web/scrapers/discourse.d.ts +0 -5
- package/dist/types/web/scrapers/dockerhub.d.ts +0 -5
- package/dist/types/web/scrapers/docs-rs.d.ts +0 -2
- package/dist/types/web/scrapers/fdroid.d.ts +0 -5
- package/dist/types/web/scrapers/firefox-addons.d.ts +0 -2
- package/dist/types/web/scrapers/flathub.d.ts +0 -2
- package/dist/types/web/scrapers/github-gist.d.ts +0 -5
- package/dist/types/web/scrapers/github.d.ts +0 -12
- package/dist/types/web/scrapers/gitlab.d.ts +0 -5
- package/dist/types/web/scrapers/go-pkg.d.ts +0 -5
- package/dist/types/web/scrapers/hackage.d.ts +0 -5
- package/dist/types/web/scrapers/hackernews.d.ts +0 -2
- package/dist/types/web/scrapers/hex.d.ts +0 -5
- package/dist/types/web/scrapers/huggingface.d.ts +0 -2
- package/dist/types/web/scrapers/iacr.d.ts +0 -5
- package/dist/types/web/scrapers/index.d.ts +0 -84
- package/dist/types/web/scrapers/jetbrains-marketplace.d.ts +0 -2
- package/dist/types/web/scrapers/lemmy.d.ts +0 -2
- package/dist/types/web/scrapers/lobsters.d.ts +0 -5
- package/dist/types/web/scrapers/mastodon.d.ts +0 -5
- package/dist/types/web/scrapers/maven.d.ts +0 -6
- package/dist/types/web/scrapers/mdn.d.ts +0 -2
- package/dist/types/web/scrapers/metacpan.d.ts +0 -5
- package/dist/types/web/scrapers/musicbrainz.d.ts +0 -5
- package/dist/types/web/scrapers/npm.d.ts +0 -5
- package/dist/types/web/scrapers/nuget.d.ts +0 -5
- package/dist/types/web/scrapers/nvd.d.ts +0 -5
- package/dist/types/web/scrapers/ollama.d.ts +0 -2
- package/dist/types/web/scrapers/open-vsx.d.ts +0 -5
- package/dist/types/web/scrapers/opencorporates.d.ts +0 -5
- package/dist/types/web/scrapers/openlibrary.d.ts +0 -5
- package/dist/types/web/scrapers/orcid.d.ts +0 -5
- package/dist/types/web/scrapers/osv.d.ts +0 -5
- package/dist/types/web/scrapers/packagist.d.ts +0 -5
- package/dist/types/web/scrapers/pub-dev.d.ts +0 -5
- package/dist/types/web/scrapers/pubmed.d.ts +0 -5
- package/dist/types/web/scrapers/pypi.d.ts +0 -5
- package/dist/types/web/scrapers/rawg.d.ts +0 -2
- package/dist/types/web/scrapers/readthedocs.d.ts +0 -2
- package/dist/types/web/scrapers/reddit.d.ts +0 -5
- package/dist/types/web/scrapers/repology.d.ts +0 -5
- package/dist/types/web/scrapers/rfc.d.ts +0 -5
- package/dist/types/web/scrapers/rubygems.d.ts +0 -5
- package/dist/types/web/scrapers/searchcode.d.ts +0 -2
- package/dist/types/web/scrapers/sec-edgar.d.ts +0 -5
- package/dist/types/web/scrapers/semantic-scholar.d.ts +0 -2
- package/dist/types/web/scrapers/snapcraft.d.ts +0 -2
- package/dist/types/web/scrapers/sourcegraph.d.ts +0 -2
- package/dist/types/web/scrapers/spdx.d.ts +0 -5
- package/dist/types/web/scrapers/spotify.d.ts +0 -8
- package/dist/types/web/scrapers/stackoverflow.d.ts +0 -6
- package/dist/types/web/scrapers/terraform.d.ts +0 -5
- package/dist/types/web/scrapers/tldr.d.ts +0 -7
- package/dist/types/web/scrapers/twitter.d.ts +0 -5
- package/dist/types/web/scrapers/types.d.ts +0 -83
- package/dist/types/web/scrapers/utils.d.ts +0 -32
- package/dist/types/web/scrapers/vimeo.d.ts +0 -5
- package/dist/types/web/scrapers/vscode-marketplace.d.ts +0 -5
- package/dist/types/web/scrapers/w3c.d.ts +0 -2
- package/dist/types/web/scrapers/wikidata.d.ts +0 -5
- package/dist/types/web/scrapers/wikipedia.d.ts +0 -5
- package/dist/types/web/scrapers/youtube.d.ts +0 -13
- package/dist/types/web/search/index.d.ts +0 -118
- package/dist/types/web/search/provider.d.ts +0 -55
- package/dist/types/web/search/providers/anthropic.d.ts +0 -32
- package/dist/types/web/search/providers/base.d.ts +0 -90
- package/dist/types/web/search/providers/brave.d.ts +0 -27
- package/dist/types/web/search/providers/codex.d.ts +0 -35
- package/dist/types/web/search/providers/duckduckgo.d.ts +0 -57
- package/dist/types/web/search/providers/exa.d.ts +0 -52
- package/dist/types/web/search/providers/gemini.d.ts +0 -57
- package/dist/types/web/search/providers/insane.d.ts +0 -53
- package/dist/types/web/search/providers/jina.d.ts +0 -26
- package/dist/types/web/search/providers/kagi.d.ts +0 -24
- package/dist/types/web/search/providers/kimi.d.ts +0 -27
- package/dist/types/web/search/providers/openai-compatible.d.ts +0 -9
- package/dist/types/web/search/providers/parallel.d.ts +0 -15
- package/dist/types/web/search/providers/perplexity.d.ts +0 -38
- package/dist/types/web/search/providers/searxng.d.ts +0 -44
- package/dist/types/web/search/providers/synthetic.d.ts +0 -21
- package/dist/types/web/search/providers/tavily.d.ts +0 -29
- package/dist/types/web/search/providers/text-citations.d.ts +0 -23
- package/dist/types/web/search/providers/utils.d.ts +0 -88
- package/dist/types/web/search/providers/xai.d.ts +0 -64
- package/dist/types/web/search/providers/zai.d.ts +0 -28
- package/dist/types/web/search/render.d.ts +0 -36
- package/dist/types/web/search/types.d.ts +0 -372
- package/dist/types/web/search/utils.d.ts +0 -4
- package/dist/types/workflow/workflow-intent-diff.d.ts +0 -25
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +0 -50
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +0 -2
- package/dist/types/workflow/workflow-intent-report.d.ts +0 -48
- package/dist/types/workspace-tree.d.ts +0 -42
- package/src/sdk.ts +0 -2623
|
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { type AgentMessage, canContinuePersistedHistory } from "@sayknow-cli/agent-core";
|
|
7
|
+
import type { ConfiguredModelChainEntry as SharedConfiguredModelChainEntry } from "@sayknow-cli/agent-core/compaction";
|
|
7
8
|
import type {
|
|
8
9
|
ImageContent,
|
|
9
10
|
Message,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
TextContent,
|
|
14
15
|
Usage,
|
|
15
16
|
} from "@sayknow-cli/ai";
|
|
17
|
+
import * as native from "@sayknow-cli/natives";
|
|
16
18
|
import { getTerminalId } from "@sayknow-cli/tui";
|
|
17
19
|
import {
|
|
18
20
|
getBlobsDir,
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
toError,
|
|
31
33
|
} from "@sayknow-cli/utils";
|
|
32
34
|
import type { TtsrInjectionRecord } from "../export/ttsr";
|
|
35
|
+
import type { ManagedLegacyLocalMigrationSource } from "../internal-urls/local-protocol";
|
|
33
36
|
import { writeTextAtomic } from "../skc-runtime/state-writer";
|
|
34
37
|
|
|
35
38
|
import * as git from "../utils/git";
|
|
@@ -51,6 +54,25 @@ import {
|
|
|
51
54
|
resolveResidentImageDataUrlSync,
|
|
52
55
|
resolveTextBlobSync,
|
|
53
56
|
} from "./blob-store";
|
|
57
|
+
import {
|
|
58
|
+
canonicalizeTrustedPath,
|
|
59
|
+
deleteManagedSessionCandidate,
|
|
60
|
+
listManagedCandidates,
|
|
61
|
+
type ManagedMigrationPolicy,
|
|
62
|
+
type ManagedScope,
|
|
63
|
+
managedDirectoryAuthorityForScope,
|
|
64
|
+
managedRootForScope,
|
|
65
|
+
openManagedCandidateForWrite,
|
|
66
|
+
prepareManagedSessionScopeForWriteSync,
|
|
67
|
+
resolveManagedScope,
|
|
68
|
+
} from "./internal/managed-session-scope";
|
|
69
|
+
import {
|
|
70
|
+
type ManagedDirectoryRoot,
|
|
71
|
+
type ManagedFileSnapshot,
|
|
72
|
+
ManagedSessionDescendantStore,
|
|
73
|
+
retainManagedDirectoryAuthority,
|
|
74
|
+
} from "./internal/managed-session-storage";
|
|
75
|
+
|
|
54
76
|
import {
|
|
55
77
|
type BashExecutionMessage,
|
|
56
78
|
type CustomMessage,
|
|
@@ -63,7 +85,9 @@ import {
|
|
|
63
85
|
sanitizeRehydratedOpenAIResponsesAssistantMessage,
|
|
64
86
|
stripInternalDetailsFields,
|
|
65
87
|
} from "./messages";
|
|
88
|
+
import { type SessionManagerReadAccess, sessionManagerReadCapability } from "./session-manager-internal";
|
|
66
89
|
import type {
|
|
90
|
+
ManagedSessionSecurityContext,
|
|
67
91
|
SessionStorage,
|
|
68
92
|
SessionStorageSnapshot,
|
|
69
93
|
SessionStorageStat,
|
|
@@ -72,9 +96,15 @@ import type {
|
|
|
72
96
|
VerifiedSessionDeleteResult,
|
|
73
97
|
VerifiedSessionDeleteTarget,
|
|
74
98
|
} from "./session-storage";
|
|
75
|
-
import { FileSessionStorage, MemorySessionStorage } from "./session-storage";
|
|
99
|
+
import { createManagedSessionSecurityContext, FileSessionStorage, MemorySessionStorage } from "./session-storage";
|
|
100
|
+
|
|
101
|
+
export const CURRENT_SESSION_VERSION = 5;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Version 5 separates persisted MCP and discovered-built-in selection authority.
|
|
105
|
+
* Version 4 patch records remain readable; older writers must not edit v5 sessions.
|
|
106
|
+
*/
|
|
76
107
|
|
|
77
|
-
export const CURRENT_SESSION_VERSION = 3;
|
|
78
108
|
function isUnderProjectSkc(cwd: string, targetPath: string): boolean {
|
|
79
109
|
const relative = path.relative(path.join(path.resolve(cwd), ".skc"), path.resolve(targetPath));
|
|
80
110
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
@@ -186,6 +216,15 @@ export function getSessionMessageViewportAnchorId(message: AgentMessage): string
|
|
|
186
216
|
return sessionMessageViewportAnchorIds.get(message);
|
|
187
217
|
}
|
|
188
218
|
|
|
219
|
+
/** Returns registered viewport anchors for durable user messages in session order. */
|
|
220
|
+
export function getUserMessageViewportAnchorIds(messages: readonly AgentMessage[]): string[] {
|
|
221
|
+
return messages.flatMap(message => {
|
|
222
|
+
if (message.role !== "user" || message.synthetic) return [];
|
|
223
|
+
const anchorId = getSessionMessageViewportAnchorId(message);
|
|
224
|
+
return anchorId ? [anchorId] : [];
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
189
228
|
export function transferSessionMessageIdentity(source: AgentMessage[], target: AgentMessage[]): void {
|
|
190
229
|
if (source.length !== target.length) {
|
|
191
230
|
throw new Error(
|
|
@@ -221,6 +260,14 @@ export interface ModelChangeEntry extends SessionEntryBase {
|
|
|
221
260
|
thinkingLevel?: string | null;
|
|
222
261
|
}
|
|
223
262
|
|
|
263
|
+
/** Persisted configured fallback chain for one model role. */
|
|
264
|
+
export type ConfiguredModelChainEntry = SharedConfiguredModelChainEntry;
|
|
265
|
+
|
|
266
|
+
export type ConfiguredModelChain = Pick<
|
|
267
|
+
ConfiguredModelChainEntry,
|
|
268
|
+
"role" | "entries" | "origin" | "identity" | "explicitHead" | "cleared"
|
|
269
|
+
>;
|
|
270
|
+
|
|
224
271
|
export interface ServiceTierChangeEntry extends SessionEntryBase {
|
|
225
272
|
type: "service_tier_change";
|
|
226
273
|
serviceTier: ServiceTier | null;
|
|
@@ -289,6 +336,18 @@ export interface MCPToolSelectionEntry extends SessionEntryBase {
|
|
|
289
336
|
type: "mcp_tool_selection";
|
|
290
337
|
/** MCP tool names selected for visibility in discovery mode. */
|
|
291
338
|
selectedToolNames: string[];
|
|
339
|
+
/** Legacy v4 combined built-in authority, retained for read compatibility. */
|
|
340
|
+
selectedDiscoveredBuiltinToolNames?: string[];
|
|
341
|
+
/** Correlates the ordered MCP and built-in entries emitted by one combined activation. */
|
|
342
|
+
mutationCorrelationId?: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Persisted discovered-built-in selection state, independent of MCP authority. */
|
|
346
|
+
export interface DiscoveredBuiltinToolSelectionEntry extends SessionEntryBase {
|
|
347
|
+
type: "discovered_builtin_tool_selection";
|
|
348
|
+
selectedToolNames: string[];
|
|
349
|
+
/** Correlates the ordered MCP and built-in entries emitted by one combined activation. */
|
|
350
|
+
mutationCorrelationId?: string;
|
|
292
351
|
}
|
|
293
352
|
|
|
294
353
|
/** Session init entry - captures initial context for subagent sessions (debugging/replay). */
|
|
@@ -352,12 +411,48 @@ export type SessionEntry =
|
|
|
352
411
|
| LabelEntry
|
|
353
412
|
| TtsrInjectionEntry
|
|
354
413
|
| MCPToolSelectionEntry
|
|
414
|
+
| DiscoveredBuiltinToolSelectionEntry
|
|
355
415
|
| SessionInitEntry
|
|
356
|
-
| ModeChangeEntry
|
|
416
|
+
| ModeChangeEntry
|
|
417
|
+
| ConfiguredModelChainEntry;
|
|
357
418
|
|
|
358
|
-
/**
|
|
419
|
+
/** Append-only replacement for mutable fields on the session header. */
|
|
420
|
+
export interface HeaderPatchRecord {
|
|
421
|
+
type: "header_patch";
|
|
422
|
+
patch: Partial<Pick<SessionHeader, "title" | "titleSource" | "cwd">>;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Append-only replacement for replay metadata on one existing session entry. */
|
|
426
|
+
export interface EntryPatchRecord {
|
|
427
|
+
type: "entry_patch";
|
|
428
|
+
entryId: string;
|
|
429
|
+
patch: Partial<Pick<SessionMessageEntry, "message">>;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export type SessionPatchRecord = HeaderPatchRecord | EntryPatchRecord;
|
|
433
|
+
|
|
434
|
+
/** Resolved file entries; patch records are applied by parseSessionEntries(). */
|
|
359
435
|
export type FileEntry = SessionHeader | SessionEntry;
|
|
360
436
|
|
|
437
|
+
export type DefaultModelSelectionStage = {
|
|
438
|
+
readonly entryRevision: number;
|
|
439
|
+
readonly headerExportRevision: number;
|
|
440
|
+
readonly sessionId: string;
|
|
441
|
+
readonly sessionFile: string | undefined;
|
|
442
|
+
readonly entries: readonly FileEntry[];
|
|
443
|
+
readonly tempPath: string | undefined;
|
|
444
|
+
readonly persistsToExistingFile: boolean;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export type DefaultModelSelectionPromotion =
|
|
448
|
+
| { readonly kind: "promoted" }
|
|
449
|
+
| { readonly kind: "not_promoted"; readonly error?: Error }
|
|
450
|
+
| { readonly kind: "unknown"; readonly error: Error };
|
|
451
|
+
|
|
452
|
+
type SessionFileReplacementSyncOutcome =
|
|
453
|
+
| { readonly kind: "replaced" }
|
|
454
|
+
| { readonly kind: "restored_previous"; readonly error: Error };
|
|
455
|
+
|
|
361
456
|
/** Tree node for getTree() - defensive copy of session structure */
|
|
362
457
|
export interface SessionTreeNode {
|
|
363
458
|
entry: SessionEntry;
|
|
@@ -372,6 +467,9 @@ export interface SessionContext {
|
|
|
372
467
|
serviceTier?: ServiceTier;
|
|
373
468
|
/** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
374
469
|
models: Record<string, string>;
|
|
470
|
+
/** Configured fallback chains for model roles on the active branch. */
|
|
471
|
+
configuredModelChains: Record<string, ConfiguredModelChain>;
|
|
472
|
+
|
|
375
473
|
/** Names of TTSR rules that have been injected this session */
|
|
376
474
|
injectedTtsrRules: string[];
|
|
377
475
|
/** Rich TTSR rule injection records for repeat resume. */
|
|
@@ -381,8 +479,12 @@ export interface SessionContext {
|
|
|
381
479
|
|
|
382
480
|
/** MCP tool names selected through discovery for this session branch. */
|
|
383
481
|
selectedMCPToolNames: string[];
|
|
482
|
+
/** Built-in discoverable tool names activated through discovery, when explicitly persisted. */
|
|
483
|
+
selectedDiscoveredBuiltinToolNames?: string[];
|
|
384
484
|
/** Whether this branch contains an explicit persisted MCP selection entry. */
|
|
385
485
|
hasPersistedMCPToolSelection: boolean;
|
|
486
|
+
/** Whether this branch contains an explicit persisted discovered-built-in selection entry. */
|
|
487
|
+
hasPersistedDiscoveredBuiltinToolSelection?: boolean;
|
|
386
488
|
/** Active mode (e.g. "plan") or "none" if no special mode is active */
|
|
387
489
|
mode: string;
|
|
388
490
|
/** Mode-specific data from the last mode_change entry */
|
|
@@ -413,11 +515,32 @@ export interface ResumeTailTerminal {
|
|
|
413
515
|
|
|
414
516
|
export interface ResumeTailError {
|
|
415
517
|
kind: "error";
|
|
416
|
-
reason: "missing" | "malformed" | "unstable" | "read-failed";
|
|
518
|
+
reason: "missing" | "malformed" | "unstable" | "read-failed" | "legacy_migration_disabled";
|
|
417
519
|
}
|
|
418
520
|
|
|
419
|
-
export type
|
|
521
|
+
export type SessionDirectoryMigrationPolicy = ManagedMigrationPolicy;
|
|
522
|
+
|
|
523
|
+
export class SessionManagedStorageError extends Error {
|
|
524
|
+
readonly code = "managed_storage_unsupported";
|
|
525
|
+
|
|
526
|
+
constructor() {
|
|
527
|
+
super(
|
|
528
|
+
"Default managed session storage requires FileSessionStorage; provide an explicit session directory for custom storage.",
|
|
529
|
+
);
|
|
530
|
+
this.name = "SessionManagedStorageError";
|
|
531
|
+
}
|
|
532
|
+
}
|
|
420
533
|
|
|
534
|
+
export class SessionMigrationPolicyError extends Error {
|
|
535
|
+
readonly code = "legacy_migration_disabled";
|
|
536
|
+
|
|
537
|
+
constructor() {
|
|
538
|
+
super("Legacy session migration is disabled for this workspace.");
|
|
539
|
+
this.name = "SessionMigrationPolicyError";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export type ResumeTailInspection = ResumeTailResumable | ResumeTailTerminal | ResumeTailError;
|
|
421
544
|
export interface StrictSessionOpenSuccess {
|
|
422
545
|
kind: "opened";
|
|
423
546
|
manager: SessionManager;
|
|
@@ -425,12 +548,47 @@ export interface StrictSessionOpenSuccess {
|
|
|
425
548
|
|
|
426
549
|
export interface StrictSessionOpenFailure {
|
|
427
550
|
kind: "error";
|
|
428
|
-
reason: ResumeTailError["reason"] | "identity-mismatch";
|
|
551
|
+
reason: ResumeTailError["reason"] | "identity-mismatch" | "migration-required";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Exact transcript bytes and authority captured from one descriptor-bound read. */
|
|
555
|
+
export interface CapturedSessionTranscriptSnapshot {
|
|
556
|
+
sourcePath: string;
|
|
557
|
+
identity: ResumeSessionIdentity;
|
|
558
|
+
content: Uint8Array;
|
|
559
|
+
storage: SessionStorage;
|
|
429
560
|
}
|
|
430
561
|
|
|
562
|
+
export type StrictSessionCaptureResult =
|
|
563
|
+
| { kind: "captured"; snapshot: CapturedSessionTranscriptSnapshot }
|
|
564
|
+
| ResumeTailError;
|
|
565
|
+
|
|
566
|
+
export type StrictSessionForkResult = { kind: "forked"; manager: SessionManager } | StrictSessionOpenFailure;
|
|
567
|
+
|
|
431
568
|
/** Result of opening an inspected session without create-or-rewrite fallback. */
|
|
432
569
|
export type StrictSessionOpenResult = StrictSessionOpenSuccess | StrictSessionOpenFailure;
|
|
433
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Capability returned only by a strict recovery hydration open. It represents
|
|
573
|
+
* immutable transcript authority and is consumed by the promotion seam.
|
|
574
|
+
*/
|
|
575
|
+
export interface RecoveryHydrationContext {
|
|
576
|
+
readonly identity: ResumeSessionIdentity;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export interface RecoveryHydrationOpenSuccess {
|
|
580
|
+
readonly kind: "hydrated";
|
|
581
|
+
readonly manager: SessionManager;
|
|
582
|
+
readonly context: RecoveryHydrationContext;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export type RecoveryHydrationOpenResult = RecoveryHydrationOpenSuccess | StrictSessionOpenFailure;
|
|
586
|
+
|
|
587
|
+
export interface RecoveryHydrationPromotionFence {
|
|
588
|
+
/** The caller has durably published ownership and acquired its writer lease. */
|
|
589
|
+
readonly ownershipReady: true;
|
|
590
|
+
}
|
|
591
|
+
|
|
434
592
|
export interface SessionInfo {
|
|
435
593
|
path: string;
|
|
436
594
|
id: string;
|
|
@@ -442,6 +600,8 @@ export interface SessionInfo {
|
|
|
442
600
|
created: Date;
|
|
443
601
|
modified: Date;
|
|
444
602
|
messageCount: number;
|
|
603
|
+
/** True when messageCount was counted from only the bounded list prefix. */
|
|
604
|
+
messageCountIsEstimate?: boolean;
|
|
445
605
|
/** File size in bytes on disk; used for compact list rendering. */
|
|
446
606
|
size: number;
|
|
447
607
|
firstMessage: string;
|
|
@@ -498,30 +658,100 @@ export type SessionManagerCloseOutcome =
|
|
|
498
658
|
| { kind: "close_failed_retryable"; error: Error }
|
|
499
659
|
| { kind: "close_unknown"; error: Error };
|
|
500
660
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
|
508
|
-
|
|
|
509
|
-
|
|
|
510
|
-
|
|
|
511
|
-
|
|
|
512
|
-
|
|
|
513
|
-
|
|
|
514
|
-
|
|
|
515
|
-
|
|
516
|
-
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
661
|
+
/** Read-only session state made available to extensions and custom tools. */
|
|
662
|
+
/** Frozen read-only session facade made available to extensions and custom tools. */
|
|
663
|
+
export interface ReadonlySessionManager {
|
|
664
|
+
getCwd(): string;
|
|
665
|
+
getSessionDir(): string;
|
|
666
|
+
getSessionId(): string;
|
|
667
|
+
getSessionFile(): string | undefined;
|
|
668
|
+
getSessionName(): string | undefined;
|
|
669
|
+
getArtifactsDir(): string | null;
|
|
670
|
+
getArtifactPath(id: string): Promise<string | null>;
|
|
671
|
+
getLeafId(): string | null;
|
|
672
|
+
getLeafEntry(): SessionEntry | undefined;
|
|
673
|
+
getEntry(id: string): SessionEntry | undefined;
|
|
674
|
+
getLabel(id: string): string | undefined;
|
|
675
|
+
getBranch(fromId?: string): SessionEntry[];
|
|
676
|
+
getHeader(): SessionHeader | null;
|
|
677
|
+
getEntries(): SessionEntry[];
|
|
678
|
+
getTree(): SessionTreeNode[];
|
|
679
|
+
getUsageStatistics(): UsageStatistics;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function cloneAndFreezeSnapshot<T>(value: T): T {
|
|
683
|
+
const cloned = cloneJsonSemantic(value);
|
|
684
|
+
const freeze = (candidate: unknown, seen = new WeakSet<object>()): void => {
|
|
685
|
+
if (candidate === null || typeof candidate !== "object" || seen.has(candidate)) return;
|
|
686
|
+
seen.add(candidate);
|
|
687
|
+
for (const child of Object.values(candidate)) freeze(child, seen);
|
|
688
|
+
Object.freeze(candidate);
|
|
689
|
+
};
|
|
690
|
+
freeze(cloned);
|
|
691
|
+
return cloned;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Creates an immutable facade that never exposes SessionManager mutation authority. */
|
|
695
|
+
export function createReadonlySessionManager(manager: SessionManager): ReadonlySessionManager {
|
|
696
|
+
const facade = Object.freeze({
|
|
697
|
+
getCwd: () => manager.getCwd(),
|
|
698
|
+
getSessionDir: () => manager.getSessionDir(),
|
|
699
|
+
getSessionId: () => manager.getSessionId(),
|
|
700
|
+
getSessionFile: () => manager.getSessionFile(),
|
|
701
|
+
getSessionName: () => manager.getSessionName(),
|
|
702
|
+
getArtifactsDir: () => manager.getArtifactsDir(),
|
|
703
|
+
getArtifactPath: (id: string) => manager.getArtifactPath(id),
|
|
704
|
+
getLeafId: () => manager.getLeafId(),
|
|
705
|
+
getLeafEntry: () => {
|
|
706
|
+
const entry = manager.getLeafEntry();
|
|
707
|
+
return entry === undefined ? undefined : cloneAndFreezeSnapshot(entry);
|
|
708
|
+
},
|
|
709
|
+
getEntry: (id: string) => {
|
|
710
|
+
const entry = manager.getEntry(id);
|
|
711
|
+
return entry === undefined ? undefined : cloneAndFreezeSnapshot(entry);
|
|
712
|
+
},
|
|
713
|
+
getLabel: (id: string) => manager.getLabel(id),
|
|
714
|
+
getBranch: (fromId?: string) => cloneAndFreezeSnapshot(manager.getBranch(fromId)),
|
|
715
|
+
getHeader: () => {
|
|
716
|
+
const header = manager.getHeader();
|
|
717
|
+
return header === null ? null : cloneAndFreezeSnapshot(header);
|
|
718
|
+
},
|
|
719
|
+
getEntries: () => cloneAndFreezeSnapshot(manager.getEntries()),
|
|
720
|
+
getTree: () => cloneAndFreezeSnapshot(manager.getTree()),
|
|
721
|
+
getUsageStatistics: () => cloneAndFreezeSnapshot(manager.getUsageStatistics()),
|
|
722
|
+
});
|
|
723
|
+
readonlySessionArtifactCapabilities.set(facade, sessionArtifactCapability(manager)!);
|
|
724
|
+
return facade;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Internal artifact-writing capability. Read-only facades expose it only through a private weak-map lookup. */
|
|
728
|
+
export type SessionArtifactCapability = Readonly<
|
|
729
|
+
Pick<SessionManager, "allocateArtifactPath" | "saveArtifact" | "putBlob">
|
|
523
730
|
>;
|
|
524
731
|
|
|
732
|
+
const sessionArtifactCapabilities = new WeakMap<SessionManager, SessionArtifactCapability>();
|
|
733
|
+
const readonlySessionArtifactCapabilities = new WeakMap<ReadonlySessionManager, SessionArtifactCapability>();
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Returns the artifact capability for a concrete persistence owner or one of its
|
|
737
|
+
* immutable read-only facades. Structural lookalikes remain unauthorized.
|
|
738
|
+
*/
|
|
739
|
+
export function sessionArtifactCapability(value: unknown): SessionArtifactCapability | undefined {
|
|
740
|
+
if (value instanceof SessionManager) {
|
|
741
|
+
let capability = sessionArtifactCapabilities.get(value);
|
|
742
|
+
if (!capability) {
|
|
743
|
+
capability = Object.freeze({
|
|
744
|
+
allocateArtifactPath: value.allocateArtifactPath.bind(value),
|
|
745
|
+
saveArtifact: value.saveArtifact.bind(value),
|
|
746
|
+
putBlob: value.putBlob.bind(value),
|
|
747
|
+
});
|
|
748
|
+
sessionArtifactCapabilities.set(value, capability);
|
|
749
|
+
}
|
|
750
|
+
return capability;
|
|
751
|
+
}
|
|
752
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
753
|
+
return readonlySessionArtifactCapabilities.get(value as ReadonlySessionManager);
|
|
754
|
+
}
|
|
525
755
|
function createSessionId(): string {
|
|
526
756
|
return Bun.randomUUIDv7();
|
|
527
757
|
}
|
|
@@ -597,18 +827,17 @@ function migrateV2ToV3(entries: FileEntry[]): void {
|
|
|
597
827
|
}
|
|
598
828
|
|
|
599
829
|
/**
|
|
600
|
-
* Run all necessary migrations to bring entries to current version.
|
|
601
|
-
*
|
|
830
|
+
* Run all necessary migrations to bring entries to the current semantic version.
|
|
831
|
+
* Version 4 adds trailing patch records, so v3 transcripts must be atomically upgraded before they can receive one.
|
|
602
832
|
*/
|
|
603
833
|
function migrateToCurrentVersion(entries: FileEntry[]): boolean {
|
|
604
834
|
const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
605
835
|
const version = header?.version ?? 1;
|
|
606
|
-
|
|
607
836
|
if (version >= CURRENT_SESSION_VERSION) return false;
|
|
608
|
-
|
|
609
837
|
if (version < 2) migrateV1ToV2(entries);
|
|
610
838
|
if (version < 3) migrateV2ToV3(entries);
|
|
611
|
-
|
|
839
|
+
const migratedHeader = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
840
|
+
if (migratedHeader) migratedHeader.version = CURRENT_SESSION_VERSION;
|
|
612
841
|
return true;
|
|
613
842
|
}
|
|
614
843
|
|
|
@@ -617,131 +846,136 @@ export function migrateSessionEntries(entries: FileEntry[]): void {
|
|
|
617
846
|
migrateToCurrentVersion(entries);
|
|
618
847
|
}
|
|
619
848
|
|
|
620
|
-
|
|
849
|
+
function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
|
|
850
|
+
const sessionsRoot = path.dirname(sessionDir);
|
|
851
|
+
const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
|
|
852
|
+
if (resolved.kind === "error") return undefined;
|
|
853
|
+
return path.resolve(sessionDir) === path.resolve(resolved.scope.directoryPath) ? sessionsRoot : undefined;
|
|
854
|
+
}
|
|
621
855
|
|
|
622
856
|
/**
|
|
623
|
-
*
|
|
624
|
-
*
|
|
857
|
+
* Resolve `target` through the real path of its nearest existing ancestor,
|
|
858
|
+
* re-appending any not-yet-created trailing segments. Symlink/reparse components
|
|
859
|
+
* in the existing prefix are canonicalized (so aliases cannot escape a containment
|
|
860
|
+
* check or alias another file), while a missing leaf (e.g. an uncommitted session
|
|
861
|
+
* transcript) is still resolved deterministically. Non-ENOENT errors (ELOOP,
|
|
862
|
+
* EACCES, ...) surface rather than silently falling back to the lexical path.
|
|
625
863
|
*/
|
|
626
|
-
function
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
864
|
+
function canonicalizeThroughExistingAncestor(target: string): string {
|
|
865
|
+
const resolved = path.resolve(target);
|
|
866
|
+
const tail: string[] = [];
|
|
867
|
+
let current = resolved;
|
|
868
|
+
while (true) {
|
|
869
|
+
try {
|
|
870
|
+
const real = fs.realpathSync(current);
|
|
871
|
+
return tail.length === 0 ? real : path.join(real, ...tail.reverse());
|
|
872
|
+
} catch (err) {
|
|
873
|
+
if (!isEnoent(err)) throw err;
|
|
874
|
+
const parent = path.dirname(current);
|
|
875
|
+
if (parent === current) return resolved;
|
|
876
|
+
tail.push(path.basename(current));
|
|
877
|
+
current = parent;
|
|
636
878
|
}
|
|
637
|
-
fs.rmSync(oldPath, { recursive: true, force: true });
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
if (existing) {
|
|
641
|
-
fs.rmSync(newPath, { recursive: true, force: true });
|
|
642
879
|
}
|
|
643
|
-
fs.renameSync(oldPath, newPath);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
|
|
647
|
-
const resolvedCwd = path.resolve(cwd);
|
|
648
|
-
return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function encodeRelativeSessionDirName(prefix: string, root: string, cwd: string): string {
|
|
652
|
-
const relative = path.relative(root, cwd).replace(/[/\\:]/g, "-");
|
|
653
|
-
return relative ? (prefix.endsWith("-") ? `${prefix}${relative}` : `${prefix}-${relative}`) : prefix;
|
|
654
880
|
}
|
|
655
881
|
|
|
656
|
-
function
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const encodedDirName = pathIsWithin(home, canonicalCwd)
|
|
662
|
-
? encodeRelativeSessionDirName("-", home, canonicalCwd)
|
|
663
|
-
: pathIsWithin(tempRoot, canonicalCwd)
|
|
664
|
-
? encodeRelativeSessionDirName("-tmp", tempRoot, canonicalCwd)
|
|
665
|
-
: encodeLegacyAbsoluteSessionDirName(canonicalCwd);
|
|
666
|
-
return { encodedDirName, resolvedCwd };
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/** Default and legacy directory candidates for maintenance-free resume discovery. */
|
|
670
|
-
function getReadOnlyDefaultSessionDirs(cwd: string, sessionsRoot: string = getSessionsDir()): string[] {
|
|
671
|
-
const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
|
|
672
|
-
const canonicalCwd = resolveEquivalentPath(resolvedCwd);
|
|
673
|
-
return [
|
|
674
|
-
path.join(sessionsRoot, encodedDirName),
|
|
675
|
-
path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(canonicalCwd)),
|
|
676
|
-
path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(resolvedCwd)),
|
|
677
|
-
].filter((dir, index, dirs) => dirs.indexOf(dir) === index);
|
|
882
|
+
function isSupportedSessionVersion(version: unknown): boolean {
|
|
883
|
+
return (
|
|
884
|
+
version === undefined ||
|
|
885
|
+
(typeof version === "number" && Number.isInteger(version) && version <= CURRENT_SESSION_VERSION)
|
|
886
|
+
);
|
|
678
887
|
}
|
|
679
888
|
|
|
680
|
-
/**
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
for (const entry of entries) {
|
|
701
|
-
let remainder: string;
|
|
702
|
-
if (entry === oldExact) {
|
|
703
|
-
remainder = "";
|
|
704
|
-
} else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
|
|
705
|
-
remainder = entry.slice(oldPrefix.length, -2);
|
|
706
|
-
} else {
|
|
889
|
+
/** Exported for compaction.test.ts */
|
|
890
|
+
export function parseSessionEntries(content: string): FileEntry[] {
|
|
891
|
+
const records = parseJsonlLenient<FileEntry | SessionPatchRecord>(content);
|
|
892
|
+
const rawHeader = records.find((record): record is SessionHeader => record.type === "session");
|
|
893
|
+
if (!isSupportedSessionVersion(rawHeader?.version)) {
|
|
894
|
+
throw new Error(`Unsupported session version: ${String(rawHeader?.version)}`);
|
|
895
|
+
}
|
|
896
|
+
const entries: FileEntry[] = [];
|
|
897
|
+
const entriesById = new Map<string, SessionEntry>();
|
|
898
|
+
let header: SessionHeader | undefined;
|
|
899
|
+
|
|
900
|
+
for (const record of records) {
|
|
901
|
+
// Patch records are valid in v4 and v5 only. Ignore patches before a valid
|
|
902
|
+
// header so malformed prefixes cannot affect later records.
|
|
903
|
+
if (record.type === "header_patch") {
|
|
904
|
+
if (header?.version !== undefined && header.version >= 4 && isHeaderPatchRecord(record))
|
|
905
|
+
applyHeaderPatch(header, record.patch);
|
|
707
906
|
continue;
|
|
708
907
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
migrateSessionDirPath(oldPath, newPath);
|
|
716
|
-
} catch {
|
|
717
|
-
// Best effort
|
|
908
|
+
if (record.type === "entry_patch") {
|
|
909
|
+
if (header?.version !== undefined && header.version >= 4 && isEntryPatchRecord(record)) {
|
|
910
|
+
const entry = entriesById.get(record.entryId);
|
|
911
|
+
if (entry?.type === "message" && record.patch.message) entry.message = record.patch.message;
|
|
912
|
+
}
|
|
913
|
+
continue;
|
|
718
914
|
}
|
|
915
|
+
entries.push(record);
|
|
916
|
+
if (record.type === "session") header ??= record;
|
|
917
|
+
else entriesById.set(record.id, record);
|
|
719
918
|
}
|
|
919
|
+
return entries;
|
|
720
920
|
}
|
|
721
921
|
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
|
|
922
|
+
function isHeaderPatchRecord(record: SessionPatchRecord): record is HeaderPatchRecord {
|
|
923
|
+
if (
|
|
924
|
+
record.type !== "header_patch" ||
|
|
925
|
+
!isRecord(record.patch) ||
|
|
926
|
+
!Object.keys(record).every(key => key === "type" || key === "patch")
|
|
927
|
+
)
|
|
928
|
+
return false;
|
|
929
|
+
const keys = Object.keys(record.patch);
|
|
930
|
+
if (!keys.every(key => key === "cwd" || key === "title" || key === "titleSource")) return false;
|
|
931
|
+
const { cwd, title, titleSource } = record.patch;
|
|
932
|
+
return (
|
|
933
|
+
(cwd === undefined || typeof cwd === "string") &&
|
|
934
|
+
(title === undefined || typeof title === "string") &&
|
|
935
|
+
(titleSource === undefined || titleSource === "auto" || titleSource === "user")
|
|
936
|
+
);
|
|
937
|
+
}
|
|
725
938
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
939
|
+
function applyHeaderPatch(header: SessionHeader, patch: HeaderPatchRecord["patch"]): void {
|
|
940
|
+
if (patch.cwd !== undefined) header.cwd = patch.cwd;
|
|
941
|
+
if (patch.title !== undefined) header.title = patch.title;
|
|
942
|
+
if (patch.titleSource !== undefined) header.titleSource = patch.titleSource;
|
|
731
943
|
}
|
|
732
944
|
|
|
733
|
-
function
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
945
|
+
function isEntryPatchRecord(record: SessionPatchRecord): record is EntryPatchRecord {
|
|
946
|
+
return (
|
|
947
|
+
record.type === "entry_patch" &&
|
|
948
|
+
typeof record.entryId === "string" &&
|
|
949
|
+
isRecord(record.patch) &&
|
|
950
|
+
Object.keys(record).every(key => key === "type" || key === "entryId" || key === "patch") &&
|
|
951
|
+
Object.keys(record.patch).every(key => key === "message") &&
|
|
952
|
+
(record.patch.message === undefined || isRecord(record.patch.message))
|
|
953
|
+
);
|
|
740
954
|
}
|
|
741
955
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
956
|
+
function normalizeConfiguredModelChainEntry(entry: unknown): ConfiguredModelChain | undefined {
|
|
957
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
958
|
+
const candidate = entry as Partial<ConfiguredModelChainEntry>;
|
|
959
|
+
if (typeof candidate.role !== "string" || candidate.role.length === 0) return undefined;
|
|
960
|
+
if (candidate.cleared === true) {
|
|
961
|
+
return {
|
|
962
|
+
role: candidate.role,
|
|
963
|
+
entries: [],
|
|
964
|
+
origin: typeof candidate.origin === "string" ? candidate.origin : "session",
|
|
965
|
+
explicitHead: candidate.explicitHead === true,
|
|
966
|
+
cleared: true,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
if (!Array.isArray(candidate.entries)) return undefined;
|
|
970
|
+
const entries = candidate.entries.filter((model): model is string => typeof model === "string" && model.length > 0);
|
|
971
|
+
if (entries.length === 0) return undefined;
|
|
972
|
+
return {
|
|
973
|
+
role: candidate.role,
|
|
974
|
+
entries,
|
|
975
|
+
origin: typeof candidate.origin === "string" ? candidate.origin : "session",
|
|
976
|
+
...(typeof candidate.identity === "string" ? { identity: candidate.identity } : {}),
|
|
977
|
+
explicitHead: candidate.explicitHead === true,
|
|
978
|
+
};
|
|
745
979
|
}
|
|
746
980
|
|
|
747
981
|
export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {
|
|
@@ -781,12 +1015,15 @@ export function buildSessionContext(
|
|
|
781
1015
|
thinkingLevel: "off",
|
|
782
1016
|
serviceTier: undefined,
|
|
783
1017
|
models: {},
|
|
1018
|
+
configuredModelChains: {},
|
|
1019
|
+
|
|
784
1020
|
injectedTtsrRules: [],
|
|
785
1021
|
injectedTtsrRuleRecords: [],
|
|
786
1022
|
ttsrMessageCount: 0,
|
|
787
1023
|
|
|
788
1024
|
selectedMCPToolNames: [],
|
|
789
1025
|
hasPersistedMCPToolSelection: false,
|
|
1026
|
+
hasPersistedDiscoveredBuiltinToolSelection: false,
|
|
790
1027
|
mode: "none",
|
|
791
1028
|
};
|
|
792
1029
|
}
|
|
@@ -804,11 +1041,14 @@ export function buildSessionContext(
|
|
|
804
1041
|
thinkingLevel: "off",
|
|
805
1042
|
serviceTier: undefined,
|
|
806
1043
|
models: {},
|
|
1044
|
+
configuredModelChains: {},
|
|
1045
|
+
|
|
807
1046
|
injectedTtsrRules: [],
|
|
808
1047
|
injectedTtsrRuleRecords: [],
|
|
809
1048
|
ttsrMessageCount: 0,
|
|
810
1049
|
selectedMCPToolNames: [],
|
|
811
1050
|
hasPersistedMCPToolSelection: false,
|
|
1051
|
+
hasPersistedDiscoveredBuiltinToolSelection: false,
|
|
812
1052
|
mode: "none",
|
|
813
1053
|
};
|
|
814
1054
|
}
|
|
@@ -829,6 +1069,8 @@ export function buildSessionContext(
|
|
|
829
1069
|
let thinkingLevel: string | undefined = "off";
|
|
830
1070
|
let serviceTier: ServiceTier | undefined;
|
|
831
1071
|
const models: Record<string, string> = {};
|
|
1072
|
+
const configuredModelChains: Record<string, ConfiguredModelChain> = {};
|
|
1073
|
+
|
|
832
1074
|
let compaction: CompactionEntry | null = null;
|
|
833
1075
|
const injectedTtsrRulesSet = new Set<string>();
|
|
834
1076
|
const injectedTtsrRuleRecords = new Map<string, TtsrInjectionRecord>();
|
|
@@ -836,6 +1078,8 @@ export function buildSessionContext(
|
|
|
836
1078
|
|
|
837
1079
|
let selectedMCPToolNames: string[] = [];
|
|
838
1080
|
let hasPersistedMCPToolSelection = false;
|
|
1081
|
+
let selectedDiscoveredBuiltinToolNames: string[] | undefined;
|
|
1082
|
+
let hasPersistedDiscoveredBuiltinToolSelection = false;
|
|
839
1083
|
let mode = "none";
|
|
840
1084
|
let modeData: Record<string, unknown> | undefined;
|
|
841
1085
|
// Track whether an explicit `model_change` with role="default" has been
|
|
@@ -859,6 +1103,13 @@ export function buildSessionContext(
|
|
|
859
1103
|
hasExplicitDefaultModel = true;
|
|
860
1104
|
}
|
|
861
1105
|
}
|
|
1106
|
+
} else if (entry.type === "configured_model_chain") {
|
|
1107
|
+
const configuredChain = normalizeConfiguredModelChainEntry(entry);
|
|
1108
|
+
if (configuredChain?.cleared) {
|
|
1109
|
+
delete configuredModelChains[configuredChain.role];
|
|
1110
|
+
} else if (configuredChain) {
|
|
1111
|
+
configuredModelChains[configuredChain.role] = configuredChain;
|
|
1112
|
+
}
|
|
862
1113
|
} else if (entry.type === "service_tier_change") {
|
|
863
1114
|
serviceTier = entry.serviceTier ?? undefined;
|
|
864
1115
|
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
@@ -889,7 +1140,14 @@ export function buildSessionContext(
|
|
|
889
1140
|
}
|
|
890
1141
|
} else if (entry.type === "mcp_tool_selection") {
|
|
891
1142
|
selectedMCPToolNames = [...entry.selectedToolNames];
|
|
1143
|
+
if (entry.selectedDiscoveredBuiltinToolNames !== undefined) {
|
|
1144
|
+
selectedDiscoveredBuiltinToolNames = [...entry.selectedDiscoveredBuiltinToolNames];
|
|
1145
|
+
hasPersistedDiscoveredBuiltinToolSelection = true;
|
|
1146
|
+
}
|
|
892
1147
|
hasPersistedMCPToolSelection = true;
|
|
1148
|
+
} else if (entry.type === "discovered_builtin_tool_selection") {
|
|
1149
|
+
selectedDiscoveredBuiltinToolNames = [...entry.selectedToolNames];
|
|
1150
|
+
hasPersistedDiscoveredBuiltinToolSelection = true;
|
|
893
1151
|
} else if (entry.type === "mode_change") {
|
|
894
1152
|
mode = entry.mode;
|
|
895
1153
|
modeData = entry.data;
|
|
@@ -897,6 +1155,16 @@ export function buildSessionContext(
|
|
|
897
1155
|
}
|
|
898
1156
|
|
|
899
1157
|
const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
|
|
1158
|
+
for (const [role, model] of Object.entries(models)) {
|
|
1159
|
+
if (role in configuredModelChains) continue;
|
|
1160
|
+
configuredModelChains[role] = {
|
|
1161
|
+
role,
|
|
1162
|
+
entries: [model],
|
|
1163
|
+
origin: "legacy_session",
|
|
1164
|
+
explicitHead: true,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
|
|
900
1168
|
const injectedTtsrRuleRecordsArray = Array.from(injectedTtsrRuleRecords.values());
|
|
901
1169
|
|
|
902
1170
|
// Build messages and collect corresponding entries
|
|
@@ -996,12 +1264,16 @@ export function buildSessionContext(
|
|
|
996
1264
|
thinkingLevel,
|
|
997
1265
|
serviceTier,
|
|
998
1266
|
models,
|
|
1267
|
+
configuredModelChains,
|
|
1268
|
+
|
|
999
1269
|
injectedTtsrRules,
|
|
1000
1270
|
injectedTtsrRuleRecords: injectedTtsrRuleRecordsArray,
|
|
1001
1271
|
ttsrMessageCount,
|
|
1002
1272
|
|
|
1003
1273
|
selectedMCPToolNames,
|
|
1274
|
+
selectedDiscoveredBuiltinToolNames,
|
|
1004
1275
|
hasPersistedMCPToolSelection,
|
|
1276
|
+
hasPersistedDiscoveredBuiltinToolSelection,
|
|
1005
1277
|
mode,
|
|
1006
1278
|
modeData,
|
|
1007
1279
|
};
|
|
@@ -1014,31 +1286,183 @@ function cloneSessionContext(context: SessionContext): SessionContext {
|
|
|
1014
1286
|
...context,
|
|
1015
1287
|
messages,
|
|
1016
1288
|
models: { ...context.models },
|
|
1289
|
+
configuredModelChains: Object.fromEntries(
|
|
1290
|
+
Object.entries(context.configuredModelChains ?? {}).map(([role, chain]) => [
|
|
1291
|
+
role,
|
|
1292
|
+
{ ...chain, entries: [...chain.entries] },
|
|
1293
|
+
]),
|
|
1294
|
+
),
|
|
1295
|
+
|
|
1017
1296
|
injectedTtsrRules: [...context.injectedTtsrRules],
|
|
1018
1297
|
injectedTtsrRuleRecords: context.injectedTtsrRuleRecords?.map(record => ({ ...record })),
|
|
1019
1298
|
ttsrMessageCount: context.ttsrMessageCount,
|
|
1020
1299
|
|
|
1021
1300
|
selectedMCPToolNames: [...context.selectedMCPToolNames],
|
|
1301
|
+
selectedDiscoveredBuiltinToolNames: context.selectedDiscoveredBuiltinToolNames
|
|
1302
|
+
? [...context.selectedDiscoveredBuiltinToolNames]
|
|
1303
|
+
: undefined,
|
|
1022
1304
|
modeData: cloneJsonSemantic(context.modeData),
|
|
1023
1305
|
};
|
|
1024
1306
|
}
|
|
1025
1307
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1308
|
+
function freezeInternalReadSnapshot<T>(value: T): T {
|
|
1309
|
+
if (process.env.NODE_ENV !== "test" && process.env.NODE_ENV !== "development") return value;
|
|
1310
|
+
const copy = cloneJsonSemantic(value);
|
|
1311
|
+
const freeze = (item: unknown): void => {
|
|
1312
|
+
if (!item || typeof item !== "object" || Object.isFrozen(item)) return;
|
|
1313
|
+
for (const child of Object.values(item as Record<string, unknown>)) freeze(child);
|
|
1314
|
+
Object.freeze(item);
|
|
1315
|
+
};
|
|
1316
|
+
freeze(copy);
|
|
1317
|
+
return copy;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/** Resolve and prepare the default v2 session scope before any managed writer exists. */
|
|
1031
1321
|
function computeDefaultSessionDir(
|
|
1032
1322
|
cwd: string,
|
|
1033
1323
|
storage: SessionStorage,
|
|
1034
1324
|
sessionsRoot: string = getSessionsDir(),
|
|
1035
1325
|
): string {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
return
|
|
1326
|
+
if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
|
|
1327
|
+
const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
|
|
1328
|
+
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
1329
|
+
const prepared = prepareManagedSessionScopeForWriteSync(resolved.scope);
|
|
1330
|
+
if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
|
|
1331
|
+
return prepared.scope.directoryPath;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/** A session directory's authority is distinct from its string path. */
|
|
1335
|
+
export type SessionDestination =
|
|
1336
|
+
| { readonly kind: "managed"; readonly directory: string; readonly securityContext: ManagedSessionSecurityContext }
|
|
1337
|
+
| { readonly kind: "explicit"; readonly directory: string };
|
|
1338
|
+
export type SessionDestinationInput = string | SessionDestination | undefined;
|
|
1339
|
+
|
|
1340
|
+
interface ForkArtifactPublication {
|
|
1341
|
+
readonly snapshot: native.NativeDirectoryTreeSnapshot;
|
|
1342
|
+
readonly store: ManagedSessionDescendantStore;
|
|
1343
|
+
readonly cleanupStore: ManagedSessionDescendantStore;
|
|
1344
|
+
readonly cleanupRelativePath: string;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function retainedTreeSnapshotEquals(
|
|
1348
|
+
left: native.NativeDirectoryTreeSnapshot,
|
|
1349
|
+
right: native.NativeDirectoryTreeSnapshot,
|
|
1350
|
+
): boolean {
|
|
1351
|
+
return (
|
|
1352
|
+
left.rootDev === right.rootDev &&
|
|
1353
|
+
left.rootIno === right.rootIno &&
|
|
1354
|
+
left.entries.length === right.entries.length &&
|
|
1355
|
+
left.entries.every((entry, index) => {
|
|
1356
|
+
const other = right.entries[index];
|
|
1357
|
+
return (
|
|
1358
|
+
other !== undefined &&
|
|
1359
|
+
entry.relativePath === other.relativePath &&
|
|
1360
|
+
entry.kind === other.kind &&
|
|
1361
|
+
entry.dev === other.dev &&
|
|
1362
|
+
entry.ino === other.ino &&
|
|
1363
|
+
entry.size === other.size &&
|
|
1364
|
+
entry.mtimeNs === other.mtimeNs &&
|
|
1365
|
+
entry.ctimeNs === other.ctimeNs &&
|
|
1366
|
+
entry.sha256 === other.sha256
|
|
1367
|
+
);
|
|
1368
|
+
})
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function retainedTreeSnapshotEqualsAfterRename(
|
|
1373
|
+
left: native.NativeDirectoryTreeSnapshot,
|
|
1374
|
+
right: native.NativeDirectoryTreeSnapshot,
|
|
1375
|
+
): boolean {
|
|
1376
|
+
return (
|
|
1377
|
+
left.rootDev === right.rootDev &&
|
|
1378
|
+
left.rootIno === right.rootIno &&
|
|
1379
|
+
left.entries.length === right.entries.length &&
|
|
1380
|
+
left.entries.every((entry, index) => {
|
|
1381
|
+
const other = right.entries[index];
|
|
1382
|
+
return (
|
|
1383
|
+
other !== undefined &&
|
|
1384
|
+
entry.relativePath === other.relativePath &&
|
|
1385
|
+
entry.kind === other.kind &&
|
|
1386
|
+
entry.dev === other.dev &&
|
|
1387
|
+
entry.ino === other.ino &&
|
|
1388
|
+
entry.size === other.size &&
|
|
1389
|
+
entry.mtimeNs === other.mtimeNs &&
|
|
1390
|
+
(entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
|
|
1391
|
+
entry.sha256 === other.sha256
|
|
1392
|
+
);
|
|
1393
|
+
})
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function managedFileSnapshotEquals(left: ManagedFileSnapshot | null, right: ManagedFileSnapshot | null): boolean {
|
|
1398
|
+
return (
|
|
1399
|
+
left !== null &&
|
|
1400
|
+
right !== null &&
|
|
1401
|
+
left.identity.dev === right.identity.dev &&
|
|
1402
|
+
left.identity.ino === right.identity.ino &&
|
|
1403
|
+
left.identity.size === right.identity.size &&
|
|
1404
|
+
left.identity.mtimeNs === right.identity.mtimeNs &&
|
|
1405
|
+
left.identity.ctimeNs === right.identity.ctimeNs &&
|
|
1406
|
+
left.identity.sha256 === right.identity.sha256 &&
|
|
1407
|
+
left.bytes.equals(right.bytes)
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
const trustedSessionDestinations = new WeakSet<SessionDestination>();
|
|
1411
|
+
|
|
1412
|
+
function managedStoreFromContext(
|
|
1413
|
+
securityContext: ManagedSessionSecurityContext,
|
|
1414
|
+
directory: string,
|
|
1415
|
+
): ManagedSessionDescendantStore {
|
|
1416
|
+
return new ManagedSessionDescendantStore(
|
|
1417
|
+
securityContext.rootAuthority,
|
|
1418
|
+
directory,
|
|
1419
|
+
securityContext.retainedAuthority
|
|
1420
|
+
? { authority: securityContext.retainedAuthority, authorityBaseDir: directory }
|
|
1421
|
+
: undefined,
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function freezeManagedDestination(scope: ManagedScope): SessionDestination {
|
|
1426
|
+
const rootAuthority = managedRootForScope(scope);
|
|
1427
|
+
const securityContext = createManagedSessionSecurityContext({
|
|
1428
|
+
agentDir: scope.agentDir,
|
|
1429
|
+
sessionsRoot: scope.sessionsRoot,
|
|
1430
|
+
sessionDir: scope.directoryPath,
|
|
1431
|
+
rootAuthority,
|
|
1432
|
+
retainedAuthority: managedDirectoryAuthorityForScope(scope),
|
|
1433
|
+
});
|
|
1434
|
+
const destination = Object.freeze({ kind: "managed" as const, directory: scope.directoryPath, securityContext });
|
|
1435
|
+
trustedSessionDestinations.add(destination);
|
|
1436
|
+
return destination;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function explicitDestination(directory: string): SessionDestination {
|
|
1440
|
+
const destination = Object.freeze({ kind: "explicit" as const, directory });
|
|
1441
|
+
trustedSessionDestinations.add(destination);
|
|
1442
|
+
return destination;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function managedDestination(cwd: string, storage: SessionStorage, agentDir?: string): SessionDestination {
|
|
1446
|
+
if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
|
|
1447
|
+
const sessionsRoot = getSessionsDir(agentDir);
|
|
1448
|
+
const resolved = resolveManagedScope({ cwd, agentDir: agentDir ?? path.resolve(sessionsRoot, ".."), sessionsRoot });
|
|
1449
|
+
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
1450
|
+
const prepared = prepareManagedSessionScopeForWriteSync(resolved.scope);
|
|
1451
|
+
if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
|
|
1452
|
+
return freezeManagedDestination(prepared.scope);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function destinationFor(
|
|
1456
|
+
cwd: string,
|
|
1457
|
+
input: SessionDestinationInput,
|
|
1458
|
+
storage: SessionStorage,
|
|
1459
|
+
agentDir?: string,
|
|
1460
|
+
): SessionDestination {
|
|
1461
|
+
if (typeof input === "object") {
|
|
1462
|
+
if (!trustedSessionDestinations.has(input)) throw new Error("Untrusted session destination authority");
|
|
1463
|
+
return input;
|
|
1464
|
+
}
|
|
1465
|
+
return input === undefined ? managedDestination(cwd, storage, agentDir) : explicitDestination(input);
|
|
1042
1466
|
}
|
|
1043
1467
|
|
|
1044
1468
|
// =============================================================================
|
|
@@ -1110,9 +1534,39 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
1110
1534
|
return null;
|
|
1111
1535
|
}
|
|
1112
1536
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1537
|
+
const inspected = inspectResumeSessionFile(sessionFile, new FileSessionStorage());
|
|
1538
|
+
if ("kind" in inspected) {
|
|
1539
|
+
if (inspected.reason !== "missing") return null;
|
|
1540
|
+
const sessionsRoot = getSessionsDir();
|
|
1541
|
+
const resolved = resolveManagedScope({
|
|
1542
|
+
cwd: breadcrumbCwd,
|
|
1543
|
+
agentDir: path.resolve(sessionsRoot, ".."),
|
|
1544
|
+
sessionsRoot,
|
|
1545
|
+
});
|
|
1546
|
+
if (resolved.kind !== "resolved") return null;
|
|
1547
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
1548
|
+
if (listing.kind !== "complete") return null;
|
|
1549
|
+
const migrated = listing.owned.filter(
|
|
1550
|
+
candidate => path.basename(candidate.path) === path.basename(sessionFile),
|
|
1551
|
+
);
|
|
1552
|
+
return migrated.length === 1 ? migrated[0]!.path : null;
|
|
1553
|
+
}
|
|
1554
|
+
const header = inspected.entries[0] as SessionHeader;
|
|
1555
|
+
if (resolveEquivalentPath(header.cwd) !== resolveEquivalentPath(breadcrumbCwd)) return null;
|
|
1556
|
+
const sessionsRoot = getSessionsDir();
|
|
1557
|
+
const resolved = resolveManagedScope({
|
|
1558
|
+
cwd: header.cwd,
|
|
1559
|
+
agentDir: path.resolve(sessionsRoot, ".."),
|
|
1560
|
+
sessionsRoot,
|
|
1561
|
+
});
|
|
1562
|
+
if (resolved.kind !== "resolved") return null;
|
|
1563
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
1564
|
+
if (listing.kind !== "complete") return null;
|
|
1565
|
+
const exact = listing.owned.find(candidate => path.resolve(candidate.path) === path.resolve(sessionFile));
|
|
1566
|
+
if (exact) return exact.path;
|
|
1567
|
+
const byIdentity = listing.owned.find(candidate => candidate.sessionId === header.id);
|
|
1568
|
+
if (byIdentity) return byIdentity.path;
|
|
1569
|
+
return pathIsWithin(sessionsRoot, path.resolve(sessionFile)) ? null : path.resolve(sessionFile);
|
|
1116
1570
|
} catch (err) {
|
|
1117
1571
|
if (!isEnoent(err)) logger.debug("Terminal breadcrumb read failed", { err });
|
|
1118
1572
|
// Breadcrumb doesn't exist or is corrupt — fall through
|
|
@@ -1132,7 +1586,7 @@ export async function loadEntriesFromFile(
|
|
|
1132
1586
|
if (isEnoent(err)) return [];
|
|
1133
1587
|
throw err;
|
|
1134
1588
|
}
|
|
1135
|
-
const entries =
|
|
1589
|
+
const entries = parseSessionEntries(content);
|
|
1136
1590
|
|
|
1137
1591
|
// Validate session header
|
|
1138
1592
|
if (entries.length === 0) return entries;
|
|
@@ -1171,6 +1625,7 @@ function sameResumeStat(left: SessionStorageStat, right: SessionStorageStat): bo
|
|
|
1171
1625
|
|
|
1172
1626
|
interface ResumeInspectionSnapshot {
|
|
1173
1627
|
identity: ResumeSessionIdentity;
|
|
1628
|
+
content: Uint8Array;
|
|
1174
1629
|
entries: FileEntry[];
|
|
1175
1630
|
context: SessionContext;
|
|
1176
1631
|
migrationApplied: boolean;
|
|
@@ -1218,6 +1673,9 @@ function hasStrictSessionSchema(entries: FileEntry[]): boolean {
|
|
|
1218
1673
|
)
|
|
1219
1674
|
return false;
|
|
1220
1675
|
break;
|
|
1676
|
+
case "configured_model_chain":
|
|
1677
|
+
if (!normalizeConfiguredModelChainEntry(value)) return false;
|
|
1678
|
+
break;
|
|
1221
1679
|
case "compaction":
|
|
1222
1680
|
if (
|
|
1223
1681
|
typeof value.summary !== "string" ||
|
|
@@ -1251,7 +1709,19 @@ function hasStrictSessionSchema(entries: FileEntry[]): boolean {
|
|
|
1251
1709
|
case "mcp_tool_selection":
|
|
1252
1710
|
if (
|
|
1253
1711
|
!Array.isArray(value.selectedToolNames) ||
|
|
1254
|
-
!value.selectedToolNames.every(name => typeof name === "string")
|
|
1712
|
+
!value.selectedToolNames.every(name => typeof name === "string") ||
|
|
1713
|
+
(value.selectedDiscoveredBuiltinToolNames !== undefined &&
|
|
1714
|
+
(!Array.isArray(value.selectedDiscoveredBuiltinToolNames) ||
|
|
1715
|
+
!value.selectedDiscoveredBuiltinToolNames.every(name => typeof name === "string"))) ||
|
|
1716
|
+
(value.mutationCorrelationId !== undefined && typeof value.mutationCorrelationId !== "string")
|
|
1717
|
+
)
|
|
1718
|
+
return false;
|
|
1719
|
+
break;
|
|
1720
|
+
case "discovered_builtin_tool_selection":
|
|
1721
|
+
if (
|
|
1722
|
+
!Array.isArray(value.selectedToolNames) ||
|
|
1723
|
+
!value.selectedToolNames.every(name => typeof name === "string") ||
|
|
1724
|
+
(value.mutationCorrelationId !== undefined && typeof value.mutationCorrelationId !== "string")
|
|
1255
1725
|
)
|
|
1256
1726
|
return false;
|
|
1257
1727
|
break;
|
|
@@ -1344,7 +1814,7 @@ function inspectResumeSessionFile(
|
|
|
1344
1814
|
mtimeNs: snapshot.mtimeNs,
|
|
1345
1815
|
sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
|
|
1346
1816
|
};
|
|
1347
|
-
return { identity, entries, context, migrationApplied };
|
|
1817
|
+
return { identity, content: Uint8Array.from(bytes), entries, context, migrationApplied };
|
|
1348
1818
|
} catch {
|
|
1349
1819
|
return { kind: "error", reason: "malformed" };
|
|
1350
1820
|
}
|
|
@@ -1577,11 +2047,22 @@ async function getSortedSessions(sessionDir: string, storage: SessionStorage): P
|
|
|
1577
2047
|
await Promise.all(
|
|
1578
2048
|
files.map(async (path: string) => {
|
|
1579
2049
|
try {
|
|
1580
|
-
const
|
|
2050
|
+
const buffer = Buffer.allocUnsafe(SESSION_LIST_PREFIX_BYTES);
|
|
2051
|
+
const content = await readSessionListPrefix(path, storage, buffer);
|
|
1581
2052
|
const entries = parseJsonlLenient<Record<string, unknown>>(content);
|
|
1582
2053
|
if (entries.length === 0) return;
|
|
1583
2054
|
const header = entries[0] as Record<string, unknown>;
|
|
1584
|
-
if (
|
|
2055
|
+
if (
|
|
2056
|
+
header.type !== "session" ||
|
|
2057
|
+
typeof header.id !== "string" ||
|
|
2058
|
+
!isSupportedSessionVersion(header.version)
|
|
2059
|
+
)
|
|
2060
|
+
return;
|
|
2061
|
+
if (typeof header.version === "number" && header.version >= 4) {
|
|
2062
|
+
for (const patch of await readSessionListTrailingPatches(path, storage, buffer)) {
|
|
2063
|
+
applySessionListHeaderPatch(header as unknown as SessionListHeader, patch);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
1585
2066
|
const mtime = storage.statSync(path).mtimeMs;
|
|
1586
2067
|
const firstPrompt = header.title ? undefined : extractFirstUserPrompt(entries);
|
|
1587
2068
|
sessions.push(new RecentSessionInfo(path, mtime, header, firstPrompt));
|
|
@@ -1618,21 +2099,223 @@ function formatTimeAgo(date: Date): string {
|
|
|
1618
2099
|
return date.toLocaleDateString();
|
|
1619
2100
|
}
|
|
1620
2101
|
|
|
1621
|
-
async function
|
|
2102
|
+
async function syncPathAndParent(filePath: string): Promise<void> {
|
|
2103
|
+
const file = await fs.promises.open(filePath, "r");
|
|
2104
|
+
try {
|
|
2105
|
+
await file.sync();
|
|
2106
|
+
} finally {
|
|
2107
|
+
await file.close();
|
|
2108
|
+
}
|
|
2109
|
+
if (process.platform !== "win32") {
|
|
2110
|
+
const parent = await fs.promises.open(path.dirname(filePath), "r");
|
|
2111
|
+
try {
|
|
2112
|
+
await parent.sync();
|
|
2113
|
+
} finally {
|
|
2114
|
+
await parent.close();
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
interface SessionMoveDirectoryHandle {
|
|
2120
|
+
sync(): Promise<void>;
|
|
2121
|
+
close(): Promise<void>;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
export async function syncSessionMoveDirectory(
|
|
2125
|
+
directory: string,
|
|
2126
|
+
platform: NodeJS.Platform = process.platform,
|
|
2127
|
+
openDirectory: (directory: string) => Promise<SessionMoveDirectoryHandle> = value => fs.promises.open(value, "r"),
|
|
2128
|
+
): Promise<void> {
|
|
2129
|
+
if (platform === "win32") return;
|
|
2130
|
+
const handle = await openDirectory(directory);
|
|
2131
|
+
try {
|
|
2132
|
+
await handle.sync();
|
|
2133
|
+
} finally {
|
|
2134
|
+
await handle.close();
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/** Durably flush every copied file and directory before an EXDEV source removal. */
|
|
2139
|
+
async function syncCopiedTree(treePath: string): Promise<void> {
|
|
2140
|
+
const stat = await fs.promises.lstat(treePath);
|
|
2141
|
+
if (stat.isSymbolicLink()) throw new Error("Refusing to fsync a copied symbolic link during a cross-device move");
|
|
2142
|
+
if (stat.isFile()) {
|
|
2143
|
+
const file = await fs.promises.open(treePath, "r");
|
|
2144
|
+
try {
|
|
2145
|
+
await file.sync();
|
|
2146
|
+
} finally {
|
|
2147
|
+
await file.close();
|
|
2148
|
+
}
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (!stat.isDirectory()) throw new Error("Copied cross-device move contains an unsupported filesystem entry");
|
|
2152
|
+
const directory = await fs.promises.opendir(treePath);
|
|
2153
|
+
try {
|
|
2154
|
+
for (;;) {
|
|
2155
|
+
const entry = await directory.read();
|
|
2156
|
+
if (entry === null) break;
|
|
2157
|
+
await syncCopiedTree(path.join(treePath, entry.name));
|
|
2158
|
+
}
|
|
2159
|
+
} finally {
|
|
2160
|
+
await directory.close();
|
|
2161
|
+
}
|
|
2162
|
+
if (process.platform !== "win32") {
|
|
2163
|
+
const handle = await fs.promises.open(treePath, "r");
|
|
2164
|
+
try {
|
|
2165
|
+
await handle.sync();
|
|
2166
|
+
} finally {
|
|
2167
|
+
await handle.close();
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
type CrossDeviceTreeIdentity = string;
|
|
2173
|
+
|
|
2174
|
+
/** Captures content plus topology; a later capture must match before source removal. */
|
|
2175
|
+
async function captureCrossDeviceTreeIdentity(treePath: string): Promise<CrossDeviceTreeIdentity> {
|
|
2176
|
+
const before = await fs.promises.lstat(treePath, { bigint: true });
|
|
2177
|
+
if (before.isSymbolicLink()) throw new Error("Refusing to move a symbolic link across devices");
|
|
2178
|
+
if (before.isFile()) {
|
|
2179
|
+
const bytes = await fs.promises.readFile(treePath);
|
|
2180
|
+
const after = await fs.promises.lstat(treePath, { bigint: true });
|
|
2181
|
+
if (
|
|
2182
|
+
before.dev !== after.dev ||
|
|
2183
|
+
before.ino !== after.ino ||
|
|
2184
|
+
before.size !== after.size ||
|
|
2185
|
+
before.mtimeNs !== after.mtimeNs
|
|
2186
|
+
)
|
|
2187
|
+
throw new Error("Cross-device move source changed while its identity was captured");
|
|
2188
|
+
return `file:${before.dev}:${before.ino}:${before.size}:${before.mtimeNs}:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
2189
|
+
}
|
|
2190
|
+
if (!before.isDirectory()) throw new Error("Cross-device move contains an unsupported filesystem entry");
|
|
2191
|
+
const directory = await fs.promises.opendir(treePath);
|
|
2192
|
+
const entries: string[] = [];
|
|
1622
2193
|
try {
|
|
1623
|
-
|
|
2194
|
+
for (;;) {
|
|
2195
|
+
const entry = await directory.read();
|
|
2196
|
+
if (entry === null) break;
|
|
2197
|
+
entries.push(entry.name);
|
|
2198
|
+
}
|
|
2199
|
+
} finally {
|
|
2200
|
+
await directory.close();
|
|
2201
|
+
}
|
|
2202
|
+
entries.sort();
|
|
2203
|
+
const children = await Promise.all(
|
|
2204
|
+
entries.map(
|
|
2205
|
+
async name => `${JSON.stringify(name)}=${await captureCrossDeviceTreeIdentity(path.join(treePath, name))}`,
|
|
2206
|
+
),
|
|
2207
|
+
);
|
|
2208
|
+
const after = await fs.promises.lstat(treePath, { bigint: true });
|
|
2209
|
+
if (
|
|
2210
|
+
before.dev !== after.dev ||
|
|
2211
|
+
before.ino !== after.ino ||
|
|
2212
|
+
before.size !== after.size ||
|
|
2213
|
+
before.mtimeNs !== after.mtimeNs
|
|
2214
|
+
)
|
|
2215
|
+
throw new Error("Cross-device move source topology changed while its identity was captured");
|
|
2216
|
+
return `directory:${before.dev}:${before.ino}:${before.size}:${before.mtimeNs}:[${children.join(",")}]`;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
async function removeCrossDeviceSourceIfUnchanged(source: string, expected: CrossDeviceTreeIdentity): Promise<void> {
|
|
2220
|
+
if ((await captureCrossDeviceTreeIdentity(source)) !== expected)
|
|
2221
|
+
throw new Error("Cross-device move source changed before removal");
|
|
2222
|
+
const stat = await fs.promises.lstat(source);
|
|
2223
|
+
if (stat.isDirectory()) await fs.promises.rm(source, { recursive: true, force: false });
|
|
2224
|
+
else await fs.promises.unlink(source);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
/**
|
|
2228
|
+
* An EXDEV retirement must be driven by a native descriptor/handle-bound
|
|
2229
|
+
* snapshot-copy-verify transaction. Do not fall back to pathname copy/remove:
|
|
2230
|
+
* that can retire a replacement after a crash or concurrent rename.
|
|
2231
|
+
*/
|
|
2232
|
+
class CrossDeviceMoveUnsupportedError extends Error {
|
|
2233
|
+
constructor(source: string, destination: string, cause?: unknown) {
|
|
2234
|
+
super(
|
|
2235
|
+
`Cross-device session move is unavailable for ${source} -> ${destination}: native atomic detach/copy/verify support is required.`,
|
|
2236
|
+
{ cause },
|
|
2237
|
+
);
|
|
2238
|
+
this.name = "CrossDeviceMoveUnsupportedError";
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
class PathMoveCompletedWithSyncFailure extends Error {
|
|
2243
|
+
constructor(readonly cause: unknown) {
|
|
2244
|
+
super("Path move completed but final source-directory sync failed.", { cause });
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
async function movePathAcrossDevicesSafe(source: string, destination: string): Promise<void> {
|
|
2249
|
+
const sourceIdentity = await captureCrossDeviceTreeIdentity(source);
|
|
2250
|
+
const renamed = native.renameNoReplacePath(source, destination);
|
|
2251
|
+
if (renamed.ok) {
|
|
2252
|
+
try {
|
|
2253
|
+
if ((await captureCrossDeviceTreeIdentity(destination)) !== sourceIdentity)
|
|
2254
|
+
throw new Error("Atomic session rename did not preserve the captured source identity");
|
|
2255
|
+
await syncSessionMoveDirectory(path.dirname(destination));
|
|
2256
|
+
if (path.dirname(source) !== path.dirname(destination)) await syncSessionMoveDirectory(path.dirname(source));
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
throw new PathMoveCompletedWithSyncFailure(error);
|
|
2259
|
+
}
|
|
1624
2260
|
return;
|
|
1625
|
-
} catch (error) {
|
|
1626
|
-
if (!hasFsCode(error, "EXDEV")) throw error;
|
|
1627
2261
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
2262
|
+
if (renamed.code === "quarantine_collision") {
|
|
2263
|
+
const error = new Error(`Session move destination already exists: ${destination}`) as NodeJS.ErrnoException;
|
|
2264
|
+
error.code = "EEXIST";
|
|
2265
|
+
throw error;
|
|
2266
|
+
}
|
|
2267
|
+
if (renamed.code !== "atomic_unavailable")
|
|
2268
|
+
throw new Error(`Atomic session rename failed: ${renamed.code ?? "unknown"}`);
|
|
2269
|
+
const sourceStat = await fs.promises.lstat(source);
|
|
2270
|
+
if (!sourceStat.isDirectory()) {
|
|
2271
|
+
let destinationCreated = false;
|
|
2272
|
+
let sourceRemoved = false;
|
|
2273
|
+
try {
|
|
2274
|
+
try {
|
|
2275
|
+
await fs.promises.link(source, destination);
|
|
2276
|
+
destinationCreated = true;
|
|
2277
|
+
} catch (error) {
|
|
2278
|
+
if (hasFsCode(error, "EXDEV")) throw new CrossDeviceMoveUnsupportedError(source, destination, error);
|
|
2279
|
+
if (
|
|
2280
|
+
!hasFsCode(error, "EPERM") &&
|
|
2281
|
+
!hasFsCode(error, "EACCES") &&
|
|
2282
|
+
!hasFsCode(error, "ENOTSUP") &&
|
|
2283
|
+
!hasFsCode(error, "EOPNOTSUPP")
|
|
2284
|
+
)
|
|
2285
|
+
throw error;
|
|
2286
|
+
await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_EXCL);
|
|
2287
|
+
destinationCreated = true;
|
|
2288
|
+
}
|
|
2289
|
+
await syncPathAndParent(destination);
|
|
2290
|
+
await removeCrossDeviceSourceIfUnchanged(source, sourceIdentity);
|
|
2291
|
+
sourceRemoved = true;
|
|
2292
|
+
await syncSessionMoveDirectory(path.dirname(source));
|
|
2293
|
+
return;
|
|
2294
|
+
} catch (error) {
|
|
2295
|
+
if (!sourceRemoved && destinationCreated) await fs.promises.unlink(destination).catch(() => undefined);
|
|
2296
|
+
if (sourceRemoved && !(error instanceof PathMoveCompletedWithSyncFailure))
|
|
2297
|
+
throw new PathMoveCompletedWithSyncFailure(error);
|
|
2298
|
+
throw error;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
const destinationParent = await fs.promises.lstat(path.dirname(destination));
|
|
2303
|
+
if (sourceStat.dev !== destinationParent.dev) throw new CrossDeviceMoveUnsupportedError(source, destination);
|
|
2304
|
+
|
|
2305
|
+
try {
|
|
1630
2306
|
await fs.promises.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
1631
|
-
await
|
|
1632
|
-
|
|
2307
|
+
await syncCopiedTree(destination);
|
|
2308
|
+
await removeCrossDeviceSourceIfUnchanged(source, sourceIdentity);
|
|
2309
|
+
try {
|
|
2310
|
+
await syncSessionMoveDirectory(path.dirname(source));
|
|
2311
|
+
} catch (error) {
|
|
2312
|
+
throw new PathMoveCompletedWithSyncFailure(error);
|
|
2313
|
+
}
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
if (!(error instanceof PathMoveCompletedWithSyncFailure))
|
|
2316
|
+
await fs.promises.rm(destination, { recursive: true, force: true }).catch(() => undefined);
|
|
2317
|
+
throw error;
|
|
1633
2318
|
}
|
|
1634
|
-
await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_EXCL);
|
|
1635
|
-
await fs.promises.unlink(source);
|
|
1636
2319
|
}
|
|
1637
2320
|
|
|
1638
2321
|
const MAX_PERSIST_CHARS = 500_000;
|
|
@@ -1984,19 +2667,12 @@ function cloneJsonSemantic<T>(value: T): T {
|
|
|
1984
2667
|
return cloned as T;
|
|
1985
2668
|
}
|
|
1986
2669
|
|
|
1987
|
-
function cloneAgentMessage<T extends AgentMessage>(message: T): T {
|
|
1988
|
-
const cloned = {
|
|
1989
|
-
...message,
|
|
1990
|
-
...("content" in message ? { content: cloneJsonSemantic(message.content) } : {}),
|
|
1991
|
-
...("providerPayload" in message ? { providerPayload: cloneJsonSemantic(message.providerPayload) } : {}),
|
|
1992
|
-
} as T;
|
|
1993
|
-
transferSessionMessageIdentity([message], [cloned]);
|
|
1994
|
-
return cloned;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
2670
|
function cloneSessionEntry(entry: SessionEntry): SessionEntry {
|
|
1998
|
-
|
|
1999
|
-
|
|
2671
|
+
const cloned = cloneJsonSemantic(entry);
|
|
2672
|
+
if (entry.type === "message" && cloned.type === "message") {
|
|
2673
|
+
transferSessionMessageIdentity([entry.message], [cloned.message]);
|
|
2674
|
+
}
|
|
2675
|
+
return cloned;
|
|
2000
2676
|
}
|
|
2001
2677
|
|
|
2002
2678
|
function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: ResidentBlobStores): SessionEntry {
|
|
@@ -2580,11 +3256,16 @@ class NdjsonFileWriter {
|
|
|
2580
3256
|
#onError: ((err: Error) => void) | undefined;
|
|
2581
3257
|
#closeDrained = false;
|
|
2582
3258
|
|
|
2583
|
-
constructor(
|
|
3259
|
+
constructor(
|
|
3260
|
+
storage: SessionStorage,
|
|
3261
|
+
path: string,
|
|
3262
|
+
options?: { flags?: "a" | "w"; onError?: (err: Error) => void; securityContext?: ManagedSessionSecurityContext },
|
|
3263
|
+
) {
|
|
2584
3264
|
this.#onError = options?.onError;
|
|
2585
3265
|
this.#writer = storage.openWriter(path, {
|
|
2586
3266
|
flags: options?.flags ?? "a",
|
|
2587
3267
|
onError: (err: Error) => this.#recordError(err),
|
|
3268
|
+
securityContext: options?.securityContext,
|
|
2588
3269
|
});
|
|
2589
3270
|
}
|
|
2590
3271
|
|
|
@@ -2618,7 +3299,7 @@ class NdjsonFileWriter {
|
|
|
2618
3299
|
}
|
|
2619
3300
|
|
|
2620
3301
|
/** Queue a write. Returns a promise so callers can await if needed. */
|
|
2621
|
-
write(entry: FileEntry): Promise<void> {
|
|
3302
|
+
write(entry: FileEntry | SessionPatchRecord): Promise<void> {
|
|
2622
3303
|
if (this.#closed || this.#closing) throw new Error("Writer closed");
|
|
2623
3304
|
if (this.#error) throw this.#error;
|
|
2624
3305
|
const line = `${JSON.stringify(entry)}\n`;
|
|
@@ -2634,7 +3315,7 @@ class NdjsonFileWriter {
|
|
|
2634
3315
|
* queue. Use only when no concurrent async write is in flight (the session-manager
|
|
2635
3316
|
* persist path enforces this via `#flushed`/`#needsFullRewriteOnNextPersist`).
|
|
2636
3317
|
*/
|
|
2637
|
-
writeSync(entry: FileEntry): void {
|
|
3318
|
+
writeSync(entry: FileEntry | SessionPatchRecord): void {
|
|
2638
3319
|
if (this.#closed || this.#closing) throw new Error("Writer closed");
|
|
2639
3320
|
if (this.#error) throw this.#error;
|
|
2640
3321
|
const line = `${JSON.stringify(entry)}\n`;
|
|
@@ -2829,6 +3510,14 @@ function extractTextFromContent(content: Message["content"]): string {
|
|
|
2829
3510
|
}
|
|
2830
3511
|
|
|
2831
3512
|
const SESSION_LIST_PREFIX_BYTES = 4096;
|
|
3513
|
+
const SESSION_LIST_TRAILING_PATCH_BYTES = 4096;
|
|
3514
|
+
// Hard cap on how many trailing bytes the listers may scan for header patches.
|
|
3515
|
+
// moveTo folds cwd patches into line 1 via a full atomic rewrite, so only a
|
|
3516
|
+
// recent title patch can live unfolded near EOF; anything buried deeper is
|
|
3517
|
+
// folded on the next natural full rewrite. Keeping this bounded preserves the
|
|
3518
|
+
// O(SESSION_LIST_PREFIX_BYTES) listing cost that SESSION_LIST_PREFIX_BYTES exists for.
|
|
3519
|
+
const SESSION_LIST_TRAILING_PATCH_SCAN_CAP = SESSION_LIST_TRAILING_PATCH_BYTES * 4;
|
|
3520
|
+
const SESSION_NAME_MAX_CHARS = 1_000;
|
|
2832
3521
|
const SESSION_LIST_PARALLEL_THRESHOLD = 64;
|
|
2833
3522
|
const SESSION_LIST_MAX_WORKERS = 16;
|
|
2834
3523
|
const sessionListPrefixDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
@@ -2848,7 +3537,70 @@ async function readSessionListPrefix(file: string, storage: SessionStorage, buff
|
|
|
2848
3537
|
}
|
|
2849
3538
|
}
|
|
2850
3539
|
|
|
2851
|
-
function
|
|
3540
|
+
async function readSessionListTrailingPatches(
|
|
3541
|
+
file: string,
|
|
3542
|
+
storage: SessionStorage,
|
|
3543
|
+
buffer: Buffer,
|
|
3544
|
+
): Promise<HeaderPatchRecord["patch"][]> {
|
|
3545
|
+
if (!(storage instanceof FileSessionStorage)) {
|
|
3546
|
+
const content = await storage.readText(file);
|
|
3547
|
+
const entries = parseSessionEntries(content);
|
|
3548
|
+
const header = entries[0] as SessionHeader | undefined;
|
|
3549
|
+
return header?.type === "session" ? [{ cwd: header.cwd, title: header.title }] : [];
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
const size = storage.statSync(file).size;
|
|
3553
|
+
if (size <= SESSION_LIST_PREFIX_BYTES) return [];
|
|
3554
|
+
const latest: HeaderPatchRecord["patch"] = {};
|
|
3555
|
+
let position = size;
|
|
3556
|
+
const scanFloor = Math.max(0, size - SESSION_LIST_TRAILING_PATCH_SCAN_CAP);
|
|
3557
|
+
let trailingFragment = Buffer.alloc(0);
|
|
3558
|
+
const chunkSize = Math.min(buffer.byteLength, SESSION_LIST_TRAILING_PATCH_BYTES);
|
|
3559
|
+
const handle = await fs.promises.open(file, "r");
|
|
3560
|
+
try {
|
|
3561
|
+
while (position > scanFloor && (latest.cwd === undefined || latest.title === undefined)) {
|
|
3562
|
+
const start = Math.max(0, position - chunkSize);
|
|
3563
|
+
const length = position - start;
|
|
3564
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
3565
|
+
const combined = Buffer.concat([buffer.subarray(0, bytesRead), trailingFragment]);
|
|
3566
|
+
let complete = combined;
|
|
3567
|
+
if (start > 0) {
|
|
3568
|
+
const firstNewline = combined.indexOf(0x0a);
|
|
3569
|
+
if (firstNewline === -1) {
|
|
3570
|
+
trailingFragment = combined;
|
|
3571
|
+
position = start;
|
|
3572
|
+
continue;
|
|
3573
|
+
}
|
|
3574
|
+
trailingFragment = combined.subarray(0, firstNewline);
|
|
3575
|
+
complete = combined.subarray(firstNewline + 1);
|
|
3576
|
+
}
|
|
3577
|
+
const lines = complete.toString("utf8").split("\n");
|
|
3578
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
3579
|
+
const line = lines[index];
|
|
3580
|
+
if (!line) continue;
|
|
3581
|
+
try {
|
|
3582
|
+
const record = JSON.parse(line) as SessionPatchRecord;
|
|
3583
|
+
if (!isHeaderPatchRecord(record)) continue;
|
|
3584
|
+
if (latest.cwd === undefined && typeof record.patch.cwd === "string") latest.cwd = record.patch.cwd;
|
|
3585
|
+
if (latest.title === undefined && typeof record.patch.title === "string")
|
|
3586
|
+
latest.title = record.patch.title;
|
|
3587
|
+
} catch {
|
|
3588
|
+
// Ignore malformed or partial records exactly as the canonical loader does.
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
position = start;
|
|
3592
|
+
}
|
|
3593
|
+
return latest.cwd === undefined && latest.title === undefined ? [] : [latest];
|
|
3594
|
+
} finally {
|
|
3595
|
+
await handle.close();
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
function applySessionListHeaderPatch(header: SessionListHeader, patch: HeaderPatchRecord["patch"]): void {
|
|
3600
|
+
if (typeof patch.cwd === "string") header.cwd = patch.cwd;
|
|
3601
|
+
if (typeof patch.title === "string") header.title = patch.title;
|
|
3602
|
+
}
|
|
3603
|
+
function decodeJsonStringFragment(value: string): string {
|
|
2852
3604
|
const safeValue = value.endsWith("\\") ? value.slice(0, -1) : value;
|
|
2853
3605
|
try {
|
|
2854
3606
|
return JSON.parse(`"${safeValue}"`) as string;
|
|
@@ -2931,6 +3683,8 @@ function extractFirstUserMessageFromPrefix(content: string): string | undefined
|
|
|
2931
3683
|
interface SessionListHeader {
|
|
2932
3684
|
type: "session";
|
|
2933
3685
|
id: string;
|
|
3686
|
+
version?: number;
|
|
3687
|
+
|
|
2934
3688
|
cwd?: string;
|
|
2935
3689
|
title?: string;
|
|
2936
3690
|
parentSession?: string;
|
|
@@ -2946,6 +3700,7 @@ function parseSessionListHeader(
|
|
|
2946
3700
|
return {
|
|
2947
3701
|
type: "session",
|
|
2948
3702
|
id: parsedHeader.id,
|
|
3703
|
+
version: typeof parsedHeader.version === "number" ? parsedHeader.version : undefined,
|
|
2949
3704
|
cwd: typeof parsedHeader.cwd === "string" ? parsedHeader.cwd : undefined,
|
|
2950
3705
|
title: typeof parsedHeader.title === "string" ? parsedHeader.title : undefined,
|
|
2951
3706
|
parentSession: typeof parsedHeader.parentSession === "string" ? parsedHeader.parentSession : undefined,
|
|
@@ -2963,6 +3718,7 @@ function parseSessionListHeader(
|
|
|
2963
3718
|
return {
|
|
2964
3719
|
type: "session",
|
|
2965
3720
|
id,
|
|
3721
|
+
version: Number(extractStringProperty(firstLine, "version")) || undefined,
|
|
2966
3722
|
cwd: extractStringProperty(firstLine, "cwd"),
|
|
2967
3723
|
title: extractStringProperty(firstLine, "title"),
|
|
2968
3724
|
parentSession: extractStringProperty(firstLine, "parentSession"),
|
|
@@ -2986,9 +3742,14 @@ async function collectSessionFromFile(
|
|
|
2986
3742
|
): Promise<SessionInfo | undefined> {
|
|
2987
3743
|
try {
|
|
2988
3744
|
const content = await readSessionListPrefix(file, storage, buffer);
|
|
2989
|
-
const entries =
|
|
3745
|
+
const entries = parseSessionEntries(content).map(entry => entry as unknown as Record<string, unknown>);
|
|
2990
3746
|
const header = parseSessionListHeader(content, entries);
|
|
2991
3747
|
if (!header) return undefined;
|
|
3748
|
+
if (typeof header.version === "number" && header.version >= 4 && header.version <= CURRENT_SESSION_VERSION) {
|
|
3749
|
+
for (const patch of await readSessionListTrailingPatches(file, storage, buffer)) {
|
|
3750
|
+
applySessionListHeaderPatch(header, patch);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
2992
3753
|
|
|
2993
3754
|
let parsedMessageCount = 0;
|
|
2994
3755
|
let firstMessage = "";
|
|
@@ -3031,6 +3792,7 @@ async function collectSessionFromFile(
|
|
|
3031
3792
|
created: new Date(header.timestamp ?? ""),
|
|
3032
3793
|
modified: stats.mtime,
|
|
3033
3794
|
messageCount,
|
|
3795
|
+
messageCountIsEstimate: stats.size > SESSION_LIST_PREFIX_BYTES,
|
|
3034
3796
|
size: stats.size,
|
|
3035
3797
|
firstMessage: firstMessage || "(no messages)",
|
|
3036
3798
|
allMessagesText: allMessages.length > 0 ? allMessages.join(" ") : firstMessage,
|
|
@@ -3131,6 +3893,7 @@ interface SessionManagerStateSnapshot {
|
|
|
3131
3893
|
titleSource: "auto" | "user" | undefined;
|
|
3132
3894
|
sessionFile: string | undefined;
|
|
3133
3895
|
flushed: boolean;
|
|
3896
|
+
ensuredOnDisk: boolean;
|
|
3134
3897
|
needsFullRewriteOnNextPersist: boolean;
|
|
3135
3898
|
fileEntries: FileEntry[];
|
|
3136
3899
|
materializedFileEntries: FileEntry[];
|
|
@@ -3146,6 +3909,7 @@ export class SessionManager {
|
|
|
3146
3909
|
#flushed: boolean = false;
|
|
3147
3910
|
#needsFullRewriteOnNextPersist: boolean = false;
|
|
3148
3911
|
#ensuredOnDisk: boolean = false;
|
|
3912
|
+
#recoveryHydrationContext: RecoveryHydrationContext | undefined;
|
|
3149
3913
|
#fileEntries: FileEntry[] = [];
|
|
3150
3914
|
#byId: Map<string, SessionEntry> = new Map();
|
|
3151
3915
|
#labelsById: Map<string, string> = new Map();
|
|
@@ -3163,8 +3927,10 @@ export class SessionManager {
|
|
|
3163
3927
|
#persistChain: Promise<void> = Promise.resolve();
|
|
3164
3928
|
#persistError: Error | undefined;
|
|
3165
3929
|
#persistErrorReported = false;
|
|
3930
|
+
|
|
3166
3931
|
#artifactManager: ArtifactManager | null = null;
|
|
3167
3932
|
#artifactManagerSessionFile: string | null = null;
|
|
3933
|
+
#managedTranscriptStoreCache: { directory: string; store: ManagedSessionDescendantStore } | null = null;
|
|
3168
3934
|
// When set, take precedence over the lazily-derived per-session manager.
|
|
3169
3935
|
// Subagents adopt the parent's manager so artifact IDs are unique across the
|
|
3170
3936
|
// whole agent tree and all files land in the parent's artifacts dir.
|
|
@@ -3185,7 +3951,7 @@ export class SessionManager {
|
|
|
3185
3951
|
#replayMetadataRevision = 0;
|
|
3186
3952
|
#materializedEntriesRevision = -1;
|
|
3187
3953
|
#materializedEntriesCache: SessionEntry[] | undefined;
|
|
3188
|
-
#sessionContextCache:
|
|
3954
|
+
#sessionContextCache: SessionContext | undefined;
|
|
3189
3955
|
#sessionContextEntryRevision = -1;
|
|
3190
3956
|
#sessionContextLeafRevision = -1;
|
|
3191
3957
|
#sessionContextReplayMetadataRevision = -1;
|
|
@@ -3199,17 +3965,30 @@ export class SessionManager {
|
|
|
3199
3965
|
#getEntriesMaterializerCallCount = 0;
|
|
3200
3966
|
#materializedEntriesCachePopulateCount = 0;
|
|
3201
3967
|
#pathOnlyContextBuildCount = 0;
|
|
3968
|
+
#internalReadAccess: SessionManagerReadAccess = {
|
|
3969
|
+
getEntries: () => freezeInternalReadSnapshot(this.#getMaterializedEntriesInternal()),
|
|
3970
|
+
getSessionContext: () => this.#getSessionContextForRead(),
|
|
3971
|
+
getTree: () => this.#getTree(freezeInternalReadSnapshot(this.#getMaterializedEntriesInternal())),
|
|
3972
|
+
};
|
|
3202
3973
|
|
|
3203
3974
|
private constructor(
|
|
3204
3975
|
private cwd: string,
|
|
3205
3976
|
private sessionDir: string,
|
|
3206
3977
|
private readonly persist: boolean,
|
|
3207
3978
|
private readonly storage: SessionStorage,
|
|
3979
|
+
private destination: SessionDestination = explicitDestination(sessionDir),
|
|
3208
3980
|
) {
|
|
3209
3981
|
this.#blobStore = persist ? new BlobStore(getBlobsDir()) : this.#residentTextBlobStore;
|
|
3210
3982
|
this.#residentImageBlobStore = this.#blobStore;
|
|
3211
3983
|
if (persist && sessionDir) {
|
|
3212
3984
|
this.storage.ensureDirSync(sessionDir);
|
|
3985
|
+
// Canonicalize the trusted session directory (single choke point for every
|
|
3986
|
+
// creation path: create/open/moveTo/fork/SDK) so benign ancestor symlinks
|
|
3987
|
+
// (e.g. macOS `/var -> /private/var`, a symlinked `$HOME`) are resolved to a
|
|
3988
|
+
// symlink-free root before the strict owner-only and reparse guards run.
|
|
3989
|
+
if (this.storage instanceof FileSessionStorage) {
|
|
3990
|
+
this.sessionDir = canonicalizeTrustedPath(sessionDir);
|
|
3991
|
+
}
|
|
3213
3992
|
}
|
|
3214
3993
|
// Note: call _initSession() or _initSessionFile() after construction
|
|
3215
3994
|
}
|
|
@@ -3302,7 +4081,12 @@ export class SessionManager {
|
|
|
3302
4081
|
|
|
3303
4082
|
#resetResidentTextBlobStore(): void {
|
|
3304
4083
|
this.#disposeResidentTextBlobStore();
|
|
3305
|
-
if (
|
|
4084
|
+
if (
|
|
4085
|
+
this.destination.kind === "explicit" &&
|
|
4086
|
+
this.persist &&
|
|
4087
|
+
this.#sessionFile &&
|
|
4088
|
+
this.storage instanceof FileSessionStorage
|
|
4089
|
+
) {
|
|
3306
4090
|
this.#residentTextBlobStore = new EphemeralBlobStore(this.#residentCacheDir(this.#sessionFile));
|
|
3307
4091
|
}
|
|
3308
4092
|
}
|
|
@@ -3323,6 +4107,7 @@ export class SessionManager {
|
|
|
3323
4107
|
titleSource: this.#titleSource,
|
|
3324
4108
|
sessionFile: this.#sessionFile,
|
|
3325
4109
|
flushed: this.#flushed,
|
|
4110
|
+
ensuredOnDisk: this.#ensuredOnDisk,
|
|
3326
4111
|
needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
|
|
3327
4112
|
// Snapshot entry objects by reference: switch/reload replaces the active entry array,
|
|
3328
4113
|
// so rollback does not need structured cloning of extension/custom details.
|
|
@@ -3335,15 +4120,23 @@ export class SessionManager {
|
|
|
3335
4120
|
|
|
3336
4121
|
restoreState(snapshot: SessionManagerStateSnapshot): void {
|
|
3337
4122
|
const restoredFileEntries = [...snapshot.materializedFileEntries];
|
|
4123
|
+
const retainsPersistWriter =
|
|
4124
|
+
this.#persistWriter?.isOpen() === true &&
|
|
4125
|
+
this.#sessionId === snapshot.sessionId &&
|
|
4126
|
+
this.#sessionFile === snapshot.sessionFile &&
|
|
4127
|
+
this.#persistWriterPath === snapshot.sessionFile;
|
|
3338
4128
|
this.#sessionId = snapshot.sessionId;
|
|
3339
4129
|
this.#sessionName = snapshot.sessionName;
|
|
3340
4130
|
this.#titleSource = snapshot.titleSource;
|
|
3341
4131
|
this.#sessionFile = snapshot.sessionFile;
|
|
3342
4132
|
this.#flushed = snapshot.flushed;
|
|
4133
|
+
this.#ensuredOnDisk = snapshot.ensuredOnDisk;
|
|
3343
4134
|
this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
|
|
3344
4135
|
this.#fileEntries = restoredFileEntries;
|
|
3345
|
-
|
|
3346
|
-
|
|
4136
|
+
if (!retainsPersistWriter) {
|
|
4137
|
+
this.#persistWriter = undefined;
|
|
4138
|
+
this.#persistWriterPath = undefined;
|
|
4139
|
+
}
|
|
3347
4140
|
this.#persistChain = Promise.resolve();
|
|
3348
4141
|
this.#persistError = undefined;
|
|
3349
4142
|
this.#persistErrorReported = false;
|
|
@@ -3364,19 +4157,19 @@ export class SessionManager {
|
|
|
3364
4157
|
}
|
|
3365
4158
|
|
|
3366
4159
|
async #hydrateExistingSession(sessionFile: string, entries: FileEntry[], migrationApplied: boolean): Promise<void> {
|
|
3367
|
-
const
|
|
3368
|
-
|
|
4160
|
+
const ownedEntries = structuredClone(entries) as FileEntry[];
|
|
4161
|
+
const header = ownedEntries[0] as SessionHeader;
|
|
4162
|
+
this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
|
|
3369
4163
|
this.#sessionId = header.id;
|
|
3370
4164
|
this.#sessionName = header.title;
|
|
3371
4165
|
this.#titleSource = header.titleSource;
|
|
3372
4166
|
this.#needsFullRewriteOnNextPersist = migrationApplied;
|
|
3373
|
-
await resolveBlobRefsInEntries(
|
|
3374
|
-
this.#fileEntries =
|
|
4167
|
+
await resolveBlobRefsInEntries(ownedEntries, this.#blobStore);
|
|
4168
|
+
this.#fileEntries = ownedEntries;
|
|
3375
4169
|
this.#resetResidentTextBlobStore();
|
|
3376
4170
|
this.#fileEntries = this.#fileEntries.map(entry =>
|
|
3377
4171
|
prepareEntryForResidentSync(entry, this.#residentBlobStores()),
|
|
3378
4172
|
);
|
|
3379
|
-
this.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
3380
4173
|
this.#buildIndex();
|
|
3381
4174
|
this.#bumpAllRevisions();
|
|
3382
4175
|
this.#flushed = true;
|
|
@@ -3394,7 +4187,7 @@ export class SessionManager {
|
|
|
3394
4187
|
await this.#closePersistWriter();
|
|
3395
4188
|
this.#persistError = undefined;
|
|
3396
4189
|
this.#persistErrorReported = false;
|
|
3397
|
-
this.#sessionFile = path.resolve(sessionFile);
|
|
4190
|
+
this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
|
|
3398
4191
|
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
3399
4192
|
this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
|
|
3400
4193
|
if (this.#fileEntries.length > 0) {
|
|
@@ -3410,12 +4203,12 @@ export class SessionManager {
|
|
|
3410
4203
|
this.#fileEntries = this.#fileEntries.map(entry =>
|
|
3411
4204
|
prepareEntryForResidentSync(entry, this.#residentBlobStores()),
|
|
3412
4205
|
);
|
|
3413
|
-
this.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
3414
4206
|
|
|
3415
4207
|
this.#buildIndex();
|
|
3416
4208
|
this.#bumpAllRevisions();
|
|
3417
4209
|
this.#flushed = true;
|
|
3418
4210
|
this.#ensuredOnDisk = true;
|
|
4211
|
+
await this.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
|
|
3419
4212
|
} else {
|
|
3420
4213
|
const explicitPath = this.#sessionFile;
|
|
3421
4214
|
this.#newSessionSync();
|
|
@@ -3437,11 +4230,75 @@ export class SessionManager {
|
|
|
3437
4230
|
return sessionFile;
|
|
3438
4231
|
}
|
|
3439
4232
|
|
|
3440
|
-
/**
|
|
4233
|
+
/** Tombstone and exact-delete managed transcripts, detaching the active transcript first. */
|
|
3441
4234
|
async dropSession(sessionPath: string): Promise<void> {
|
|
4235
|
+
const requestedPath = path.resolve(sessionPath);
|
|
4236
|
+
if (this.#sessionFile && path.resolve(this.#sessionFile) === requestedPath) {
|
|
4237
|
+
await this.newSession();
|
|
4238
|
+
}
|
|
4239
|
+
await this.#closePersistWriter();
|
|
4240
|
+
const managedRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
|
|
4241
|
+
try {
|
|
4242
|
+
if (managedRoot) {
|
|
4243
|
+
if (!pathIsWithin(managedRoot, requestedPath))
|
|
4244
|
+
throw new Error("Managed session deletion is limited to this manager's configured session root.");
|
|
4245
|
+
const resolved = resolveManagedScope({
|
|
4246
|
+
cwd: this.cwd,
|
|
4247
|
+
agentDir: path.resolve(managedRoot, ".."),
|
|
4248
|
+
sessionsRoot: managedRoot,
|
|
4249
|
+
});
|
|
4250
|
+
if (resolved.kind !== "resolved")
|
|
4251
|
+
throw new Error("Managed session storage could not be resolved for this manager.");
|
|
4252
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
4253
|
+
if (listing.kind !== "complete")
|
|
4254
|
+
throw new Error("Managed session storage could not be verified; refusing deletion.");
|
|
4255
|
+
const candidate = listing.owned.find(candidate => path.resolve(candidate.path) === requestedPath);
|
|
4256
|
+
if (!candidate) throw new Error("Managed session deletion requires exact logical authorization.");
|
|
4257
|
+
const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
|
|
4258
|
+
if (deleted.kind === "error") throw new Error(`Could not delete managed session: ${deleted.message}`);
|
|
4259
|
+
} else {
|
|
4260
|
+
await this.storage.deleteSessionWithArtifacts(sessionPath);
|
|
4261
|
+
}
|
|
4262
|
+
} catch (err) {
|
|
4263
|
+
if (isEnoent(err)) return;
|
|
4264
|
+
throw err;
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
/**
|
|
4269
|
+
* Exact-delete a session transcript and its artifacts by path WITHOUT requiring
|
|
4270
|
+
* managed logical authorization. This is only for discarding an UNCOMMITTED
|
|
4271
|
+
* successor that a transaction (e.g. handoff) created via `newSession()` but
|
|
4272
|
+
* never durably authorized — such a session has no managed candidate listing
|
|
4273
|
+
* yet, so `dropSession` would refuse it and leak its resident-cache/artifact
|
|
4274
|
+
* root. Bounded to this manager's configured session root and refuses to touch
|
|
4275
|
+
* the active session; tolerates missing files.
|
|
4276
|
+
*/
|
|
4277
|
+
async discardUncommittedSession(sessionPath: string): Promise<void> {
|
|
4278
|
+
// Close the writer FIRST so the canonical checks and the delete run with no
|
|
4279
|
+
// intervening await, eliminating the check/use window in which a parent
|
|
4280
|
+
// directory could be swapped for a symlink between authorization and deletion.
|
|
3442
4281
|
await this.#closePersistWriter();
|
|
4282
|
+
// Canonicalize the candidate (and the active file / configured dir) through
|
|
4283
|
+
// their nearest existing ancestor so symlink/reparse components cannot alias
|
|
4284
|
+
// the active session or escape the configured session directory, even when
|
|
4285
|
+
// the successor transcript itself does not yet exist.
|
|
4286
|
+
const canonicalCandidate = canonicalizeThroughExistingAncestor(path.resolve(sessionPath));
|
|
4287
|
+
if (this.#sessionFile) {
|
|
4288
|
+
const canonicalActive = canonicalizeThroughExistingAncestor(path.resolve(this.#sessionFile));
|
|
4289
|
+
if (canonicalActive === canonicalCandidate) {
|
|
4290
|
+
throw new Error("Refusing to discard the active session as uncommitted.");
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
// Least-authority containment: only within this manager's configured session
|
|
4294
|
+
// directory (canonical), where newSession() allocates the successor. The
|
|
4295
|
+
// broader managed root (parent of sessionDir) is deliberately NOT accepted.
|
|
4296
|
+
const canonicalSessionDir = canonicalizeThroughExistingAncestor(path.resolve(this.sessionDir));
|
|
4297
|
+
if (!pathIsWithin(canonicalSessionDir, canonicalCandidate)) {
|
|
4298
|
+
throw new Error("Uncommitted session discard is limited to this manager's configured session directory.");
|
|
4299
|
+
}
|
|
3443
4300
|
try {
|
|
3444
|
-
await this.storage.deleteSessionWithArtifacts(
|
|
4301
|
+
await this.storage.deleteSessionWithArtifacts(canonicalCandidate);
|
|
3445
4302
|
} catch (err) {
|
|
3446
4303
|
if (isEnoent(err)) return;
|
|
3447
4304
|
throw err;
|
|
@@ -3461,6 +4318,9 @@ export class SessionManager {
|
|
|
3461
4318
|
const oldSessionFile = this.#sessionFile;
|
|
3462
4319
|
const oldSessionId = this.#sessionId;
|
|
3463
4320
|
const materializedEntries = materializeResidentEntriesForReadSync(this.#fileEntries, this.#residentBlobStores());
|
|
4321
|
+
const oldFileEntries = this.#fileEntries;
|
|
4322
|
+
const oldSessionName = this.#sessionName;
|
|
4323
|
+
const oldTitleSource = this.#titleSource;
|
|
3464
4324
|
|
|
3465
4325
|
// Close the current writer
|
|
3466
4326
|
await this.#closePersistWriter();
|
|
@@ -3496,13 +4356,169 @@ export class SessionManager {
|
|
|
3496
4356
|
this.#reexternalizeFileEntriesForResidentStore();
|
|
3497
4357
|
this.#bumpAllRevisions();
|
|
3498
4358
|
|
|
3499
|
-
// Write the new session file
|
|
4359
|
+
// Write the new session file, import artifacts, and adopt only after the
|
|
4360
|
+
// retained destination manifest is complete.
|
|
3500
4361
|
this.#flushed = false;
|
|
3501
|
-
|
|
4362
|
+
let forkArtifactPublication: ForkArtifactPublication | undefined;
|
|
4363
|
+
let forkTranscriptPublication: ManagedFileSnapshot | null = null;
|
|
4364
|
+
let forkTranscriptStore: ManagedSessionDescendantStore | undefined;
|
|
4365
|
+
try {
|
|
4366
|
+
forkArtifactPublication = await this.copyArtifactsForFork(oldSessionFile, this.#sessionFile);
|
|
4367
|
+
await this.#rewriteFile();
|
|
4368
|
+
if (this.destination.kind === "managed") {
|
|
4369
|
+
forkTranscriptStore = this.#managedTranscriptStore();
|
|
4370
|
+
forkTranscriptPublication = forkTranscriptStore.readExpected(path.basename(this.#sessionFile));
|
|
4371
|
+
if (!forkTranscriptPublication) throw new Error("managed_fork_transcript_publish_missing");
|
|
4372
|
+
}
|
|
4373
|
+
forkArtifactPublication?.store.verifyRootSecurity();
|
|
4374
|
+
forkArtifactPublication?.store.assertBound();
|
|
4375
|
+
if (forkArtifactPublication) {
|
|
4376
|
+
const adoptedSnapshot = forkArtifactPublication.store.fsyncTree();
|
|
4377
|
+
if (!retainedTreeSnapshotEquals(adoptedSnapshot, forkArtifactPublication.snapshot)) {
|
|
4378
|
+
throw new Error("artifact_destination_changed_during_transcript_publication");
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
if (forkTranscriptStore && forkTranscriptPublication) {
|
|
4382
|
+
const terminalTranscript = forkTranscriptStore.readExpected(path.basename(this.#sessionFile));
|
|
4383
|
+
if (!managedFileSnapshotEquals(terminalTranscript, forkTranscriptPublication))
|
|
4384
|
+
throw new Error("managed_fork_transcript_changed");
|
|
4385
|
+
}
|
|
4386
|
+
} catch (error) {
|
|
4387
|
+
let failure = error;
|
|
4388
|
+
const failedSessionFile = this.#sessionFile;
|
|
4389
|
+
if (this.destination.kind === "managed") {
|
|
4390
|
+
try {
|
|
4391
|
+
if (forkTranscriptStore && forkTranscriptPublication)
|
|
4392
|
+
forkTranscriptStore.removeExpected(path.basename(failedSessionFile), forkTranscriptPublication);
|
|
4393
|
+
if (forkArtifactPublication)
|
|
4394
|
+
forkArtifactPublication.cleanupStore.removeTreeExpected(
|
|
4395
|
+
forkArtifactPublication.cleanupRelativePath,
|
|
4396
|
+
forkArtifactPublication.snapshot,
|
|
4397
|
+
);
|
|
4398
|
+
} catch (cleanupError) {
|
|
4399
|
+
failure = new Error(`Failed to clean up managed fork destination: ${toError(cleanupError).message}`, {
|
|
4400
|
+
cause: toError(error),
|
|
4401
|
+
});
|
|
4402
|
+
}
|
|
4403
|
+
} else {
|
|
4404
|
+
try {
|
|
4405
|
+
await this.storage.unlink(failedSessionFile);
|
|
4406
|
+
} catch {
|
|
4407
|
+
// Preserve ambiguous cleanup evidence while restoring the active manager.
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
this.#sessionId = oldSessionId;
|
|
4411
|
+
this.#sessionFile = oldSessionFile;
|
|
4412
|
+
this.#fileEntries = oldFileEntries;
|
|
4413
|
+
this.#sessionName = oldSessionName;
|
|
4414
|
+
this.#titleSource = oldTitleSource;
|
|
4415
|
+
this.#persistChain = Promise.resolve();
|
|
4416
|
+
this.#persistError = undefined;
|
|
4417
|
+
this.#persistErrorReported = false;
|
|
4418
|
+
this.#resetResidentTextBlobStore();
|
|
4419
|
+
this.#reexternalizeFileEntriesForResidentStore();
|
|
4420
|
+
this.#bumpAllRevisions();
|
|
4421
|
+
throw failure;
|
|
4422
|
+
}
|
|
3502
4423
|
|
|
3503
4424
|
return { oldSessionFile, newSessionFile: this.#sessionFile };
|
|
3504
4425
|
}
|
|
3505
4426
|
|
|
4427
|
+
async copyArtifactsForFork(
|
|
4428
|
+
oldSessionFile: string,
|
|
4429
|
+
newSessionFile: string,
|
|
4430
|
+
): Promise<ForkArtifactPublication | undefined> {
|
|
4431
|
+
const sourceDir = oldSessionFile.slice(0, -6);
|
|
4432
|
+
const finalDestinationDir = newSessionFile.slice(0, -6);
|
|
4433
|
+
if (this.destination.kind !== "managed") {
|
|
4434
|
+
try {
|
|
4435
|
+
const sourceStat = fs.lstatSync(sourceDir);
|
|
4436
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) throw new Error("unsafe_artifacts");
|
|
4437
|
+
} catch (error) {
|
|
4438
|
+
if (isEnoent(error)) return;
|
|
4439
|
+
throw error;
|
|
4440
|
+
}
|
|
4441
|
+
await fs.promises.cp(sourceDir, finalDestinationDir, { recursive: true });
|
|
4442
|
+
return undefined;
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
const sourceName = path.basename(sourceDir);
|
|
4446
|
+
const destinationName = path.basename(finalDestinationDir);
|
|
4447
|
+
const stagingName = `.${destinationName}.${crypto.randomUUID()}.fork-staging`;
|
|
4448
|
+
const parentStore = this.#managedTranscriptStore(oldSessionFile);
|
|
4449
|
+
let sourceSnapshot: native.NativeDirectoryTreeSnapshot;
|
|
4450
|
+
try {
|
|
4451
|
+
sourceSnapshot = parentStore.captureTree(sourceName);
|
|
4452
|
+
} catch (error) {
|
|
4453
|
+
if (error instanceof Error && error.message === "not_found") return;
|
|
4454
|
+
throw error;
|
|
4455
|
+
}
|
|
4456
|
+
let stagingSnapshot: native.NativeDirectoryTreeSnapshot | undefined;
|
|
4457
|
+
let publishedSnapshot: native.NativeDirectoryTreeSnapshot | undefined;
|
|
4458
|
+
try {
|
|
4459
|
+
parentStore.ensureDirectory(stagingName);
|
|
4460
|
+
stagingSnapshot = parentStore.captureTree(stagingName);
|
|
4461
|
+
for (const entry of sourceSnapshot.entries) {
|
|
4462
|
+
if (entry.relativePath === "") continue;
|
|
4463
|
+
const destinationRelativePath = path.posix.join(stagingName, entry.relativePath);
|
|
4464
|
+
if (entry.kind === "directory") {
|
|
4465
|
+
parentStore.ensureDirectory(destinationRelativePath);
|
|
4466
|
+
} else if (entry.kind === "file") {
|
|
4467
|
+
const sourceFile = parentStore.readExpected(path.posix.join(sourceName, entry.relativePath));
|
|
4468
|
+
if (
|
|
4469
|
+
!sourceFile ||
|
|
4470
|
+
sourceFile.identity.dev.toString() !== entry.dev ||
|
|
4471
|
+
sourceFile.identity.ino.toString() !== entry.ino ||
|
|
4472
|
+
sourceFile.identity.size.toString() !== entry.size ||
|
|
4473
|
+
sourceFile.identity.mtimeNs.toString() !== entry.mtimeNs ||
|
|
4474
|
+
crypto.createHash("sha256").update(sourceFile.bytes).digest("hex") !== entry.sha256
|
|
4475
|
+
) {
|
|
4476
|
+
throw new Error("artifact_source_changed");
|
|
4477
|
+
}
|
|
4478
|
+
await parentStore.publishNoReplace(destinationRelativePath, sourceFile.bytes);
|
|
4479
|
+
} else {
|
|
4480
|
+
throw new Error("unsafe_artifacts");
|
|
4481
|
+
}
|
|
4482
|
+
stagingSnapshot = parentStore.captureTree(stagingName);
|
|
4483
|
+
}
|
|
4484
|
+
if (JSON.stringify(parentStore.captureTree(sourceName)) !== JSON.stringify(sourceSnapshot))
|
|
4485
|
+
throw new Error("artifact_source_changed");
|
|
4486
|
+
parentStore.moveTreeNoReplace(stagingName, destinationName, stagingSnapshot);
|
|
4487
|
+
publishedSnapshot = parentStore.captureTree(destinationName);
|
|
4488
|
+
const comparable = (tree: native.NativeDirectoryTreeSnapshot) =>
|
|
4489
|
+
tree.entries.map(entry => ({
|
|
4490
|
+
relativePath: entry.relativePath,
|
|
4491
|
+
kind: entry.kind,
|
|
4492
|
+
size: entry.kind === "file" ? entry.size : undefined,
|
|
4493
|
+
sha256: entry.sha256,
|
|
4494
|
+
}));
|
|
4495
|
+
if (JSON.stringify(comparable(publishedSnapshot)) !== JSON.stringify(comparable(sourceSnapshot)))
|
|
4496
|
+
throw new Error("artifact_destination_terminal_mismatch");
|
|
4497
|
+
const publishedStore = parentStore.deriveSubtree(destinationName);
|
|
4498
|
+
publishedStore.verifyRootSecurity();
|
|
4499
|
+
const retainedPublishedSnapshot = publishedStore.captureTree("");
|
|
4500
|
+
if (!retainedTreeSnapshotEqualsAfterRename(retainedPublishedSnapshot, publishedSnapshot))
|
|
4501
|
+
throw new Error("artifact_destination_retained_identity_changed");
|
|
4502
|
+
publishedSnapshot = retainedPublishedSnapshot;
|
|
4503
|
+
return {
|
|
4504
|
+
snapshot: publishedSnapshot,
|
|
4505
|
+
store: publishedStore,
|
|
4506
|
+
cleanupStore: parentStore,
|
|
4507
|
+
cleanupRelativePath: destinationName,
|
|
4508
|
+
};
|
|
4509
|
+
} catch (error) {
|
|
4510
|
+
try {
|
|
4511
|
+
if (publishedSnapshot) parentStore.removeTreeExpected(destinationName, publishedSnapshot);
|
|
4512
|
+
else if (stagingSnapshot) parentStore.removeTreeExpected(stagingName, stagingSnapshot);
|
|
4513
|
+
} catch (cleanupError) {
|
|
4514
|
+
throw new Error(`Failed to clean up managed fork artifacts: ${toError(cleanupError).message}`, {
|
|
4515
|
+
cause: toError(error),
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
throw error;
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
|
|
3506
4522
|
/**
|
|
3507
4523
|
* Move the session to a new working directory.
|
|
3508
4524
|
* Moves session files and artifacts on disk, updates all internal references,
|
|
@@ -3511,12 +4527,29 @@ export class SessionManager {
|
|
|
3511
4527
|
async moveTo(newCwd: string): Promise<void> {
|
|
3512
4528
|
const resolvedCwd = path.resolve(newCwd);
|
|
3513
4529
|
if (resolvedCwd === this.cwd) return;
|
|
4530
|
+
const previousCwd = this.cwd;
|
|
4531
|
+
const previousSessionDir = this.sessionDir;
|
|
4532
|
+
const previousSessionFile = this.#sessionFile;
|
|
4533
|
+
const previousDestination = this.destination;
|
|
4534
|
+
const previousArtifactDir = previousSessionFile?.slice(0, -6);
|
|
3514
4535
|
|
|
3515
|
-
const
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
4536
|
+
const nextDestination = (() => {
|
|
4537
|
+
if (this.destination.kind === "managed") {
|
|
4538
|
+
return managedDestination(resolvedCwd, this.storage, this.destination.securityContext.agentDir);
|
|
4539
|
+
}
|
|
4540
|
+
if (!(this.storage instanceof FileSessionStorage)) return this.destination;
|
|
4541
|
+
const managedSessionsRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
|
|
4542
|
+
const directory = managedSessionsRoot
|
|
4543
|
+
? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
|
|
4544
|
+
: computeDefaultSessionDir(resolvedCwd, this.storage);
|
|
4545
|
+
return explicitDestination(directory);
|
|
4546
|
+
})();
|
|
4547
|
+
const newSessionDir = nextDestination.directory;
|
|
3519
4548
|
let hadSessionFile = false;
|
|
4549
|
+
const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
|
|
4550
|
+
let managedSourceStore: ManagedSessionDescendantStore | undefined;
|
|
4551
|
+
let managedDestinationStore: ManagedSessionDescendantStore | undefined;
|
|
4552
|
+
let rollbackManagedMove: (() => Promise<void>) | undefined;
|
|
3520
4553
|
|
|
3521
4554
|
if (this.persist && this.#sessionFile) {
|
|
3522
4555
|
// Close the persist writer before moving files
|
|
@@ -3529,7 +4562,11 @@ export class SessionManager {
|
|
|
3529
4562
|
const newSessionFile = path.join(newSessionDir, path.basename(oldSessionFile));
|
|
3530
4563
|
const oldArtifactDir = oldSessionFile.slice(0, -6); // strip .jsonl
|
|
3531
4564
|
const newArtifactDir = newSessionFile.slice(0, -6);
|
|
3532
|
-
|
|
4565
|
+
let managedTranscript!: ReturnType<ManagedSessionDescendantStore["readExpected"]>;
|
|
4566
|
+
let managedArtifacts: native.NativeDirectoryTreeSnapshot | undefined;
|
|
4567
|
+
let managedPublishedTranscript!: ReturnType<ManagedSessionDescendantStore["readExpected"]>;
|
|
4568
|
+
let managedPublishedArtifacts: native.NativeDirectoryTreeSnapshot | undefined;
|
|
4569
|
+
hadSessionFile = managedMove ? false : this.storage.existsSync(oldSessionFile);
|
|
3533
4570
|
let movedSessionFile = false;
|
|
3534
4571
|
let movedArtifactDir = false;
|
|
3535
4572
|
const materializedEntries = materializeResidentEntriesForReadSync(
|
|
@@ -3552,46 +4589,184 @@ export class SessionManager {
|
|
|
3552
4589
|
}
|
|
3553
4590
|
throw error;
|
|
3554
4591
|
};
|
|
4592
|
+
const copyManagedTree = async (
|
|
4593
|
+
source: ManagedSessionDescendantStore,
|
|
4594
|
+
sourceRelative: string,
|
|
4595
|
+
destination: ManagedSessionDescendantStore,
|
|
4596
|
+
destinationRelative: string,
|
|
4597
|
+
snapshot: native.NativeDirectoryTreeSnapshot,
|
|
4598
|
+
): Promise<void> => {
|
|
4599
|
+
destination.ensureDirectory(destinationRelative);
|
|
4600
|
+
for (const entry of snapshot.entries) {
|
|
4601
|
+
if (entry.relativePath === "") continue;
|
|
4602
|
+
const target = path.posix.join(destinationRelative, entry.relativePath);
|
|
4603
|
+
if (entry.kind === "directory") destination.ensureDirectory(target);
|
|
4604
|
+
else {
|
|
4605
|
+
const file = source.readExpected(path.posix.join(sourceRelative, entry.relativePath));
|
|
4606
|
+
if (
|
|
4607
|
+
!file ||
|
|
4608
|
+
file.identity.dev.toString() !== entry.dev ||
|
|
4609
|
+
file.identity.ino.toString() !== entry.ino ||
|
|
4610
|
+
file.identity.size.toString() !== entry.size ||
|
|
4611
|
+
file.identity.mtimeNs.toString() !== entry.mtimeNs ||
|
|
4612
|
+
crypto.createHash("sha256").update(file.bytes).digest("hex") !== entry.sha256
|
|
4613
|
+
)
|
|
4614
|
+
throw new Error("artifact_source_changed");
|
|
4615
|
+
await destination.publishNoReplace(target, file.bytes);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
const imported = destination.captureTree(destinationRelative);
|
|
4619
|
+
const comparable = (tree: native.NativeDirectoryTreeSnapshot) =>
|
|
4620
|
+
tree.entries.map(entry => ({
|
|
4621
|
+
relativePath: entry.relativePath,
|
|
4622
|
+
kind: entry.kind,
|
|
4623
|
+
size: entry.size,
|
|
4624
|
+
sha256: entry.sha256,
|
|
4625
|
+
}));
|
|
4626
|
+
if (JSON.stringify(comparable(imported)) !== JSON.stringify(comparable(snapshot)))
|
|
4627
|
+
throw new Error("artifact_destination_mismatch");
|
|
4628
|
+
destination.fsyncTree();
|
|
4629
|
+
};
|
|
3555
4630
|
this.#disposeResidentTextBlobStore();
|
|
3556
|
-
|
|
3557
4631
|
try {
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
4632
|
+
if (managedMove) {
|
|
4633
|
+
if (previousDestination.kind !== "managed" || nextDestination.kind !== "managed")
|
|
4634
|
+
throw new Error("managed_move_destination_unavailable");
|
|
4635
|
+
managedSourceStore = this.#managedTranscriptStore(oldSessionFile);
|
|
4636
|
+
managedDestinationStore = managedStoreFromContext(nextDestination.securityContext, newSessionDir);
|
|
4637
|
+
rollbackManagedMove = async () => {
|
|
4638
|
+
if (!managedSourceStore || !managedDestinationStore)
|
|
4639
|
+
throw new Error("managed_move_authority_unavailable");
|
|
4640
|
+
if (managedArtifacts && managedPublishedArtifacts) {
|
|
4641
|
+
await copyManagedTree(
|
|
4642
|
+
managedDestinationStore,
|
|
4643
|
+
path.basename(newArtifactDir),
|
|
4644
|
+
managedSourceStore,
|
|
4645
|
+
path.basename(oldArtifactDir),
|
|
4646
|
+
managedPublishedArtifacts,
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
4649
|
+
if (managedTranscript)
|
|
4650
|
+
await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
|
|
4651
|
+
if (managedPublishedArtifacts)
|
|
4652
|
+
managedDestinationStore.removeTreeExpected(
|
|
4653
|
+
path.basename(newArtifactDir),
|
|
4654
|
+
managedPublishedArtifacts,
|
|
4655
|
+
);
|
|
4656
|
+
if (managedPublishedTranscript)
|
|
4657
|
+
managedDestinationStore.removeExpected(path.basename(newSessionFile), managedPublishedTranscript);
|
|
4658
|
+
};
|
|
4659
|
+
managedTranscript = managedSourceStore.readExpected(path.basename(oldSessionFile));
|
|
4660
|
+
hadSessionFile = managedTranscript !== null;
|
|
4661
|
+
if (managedTranscript) {
|
|
4662
|
+
await managedDestinationStore.publishNoReplace(
|
|
4663
|
+
path.basename(newSessionFile),
|
|
4664
|
+
managedTranscript.bytes,
|
|
4665
|
+
);
|
|
4666
|
+
managedPublishedTranscript = managedDestinationStore.readExpected(path.basename(newSessionFile));
|
|
4667
|
+
if (!managedPublishedTranscript) throw new Error("managed_transcript_publish_missing");
|
|
4668
|
+
movedSessionFile = true;
|
|
3569
4669
|
}
|
|
3570
|
-
} catch (err) {
|
|
3571
|
-
if (!isEnoent(err)) throw err;
|
|
3572
|
-
}
|
|
3573
|
-
} catch (err) {
|
|
3574
|
-
if (movedArtifactDir) {
|
|
3575
4670
|
try {
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
),
|
|
4671
|
+
managedArtifacts = managedSourceStore.captureTree(path.basename(oldArtifactDir));
|
|
4672
|
+
await copyManagedTree(
|
|
4673
|
+
managedSourceStore,
|
|
4674
|
+
path.basename(oldArtifactDir),
|
|
4675
|
+
managedDestinationStore,
|
|
4676
|
+
path.basename(newArtifactDir),
|
|
4677
|
+
managedArtifacts,
|
|
3582
4678
|
);
|
|
4679
|
+
managedPublishedArtifacts = managedDestinationStore.captureTree(path.basename(newArtifactDir));
|
|
4680
|
+
movedArtifactDir = true;
|
|
4681
|
+
} catch (error) {
|
|
4682
|
+
if (!(error instanceof Error) || error.message !== "not_found") throw error;
|
|
4683
|
+
}
|
|
4684
|
+
if (managedPublishedTranscript) {
|
|
4685
|
+
const terminalTranscript = managedDestinationStore.readExpected(path.basename(newSessionFile));
|
|
4686
|
+
if (!managedFileSnapshotEquals(terminalTranscript, managedPublishedTranscript))
|
|
4687
|
+
throw new Error("managed_move_destination_transcript_changed");
|
|
4688
|
+
}
|
|
4689
|
+
if (managedPublishedArtifacts) {
|
|
4690
|
+
const terminalArtifacts = managedDestinationStore.captureTree(path.basename(newArtifactDir));
|
|
4691
|
+
if (!retainedTreeSnapshotEquals(terminalArtifacts, managedPublishedArtifacts))
|
|
4692
|
+
throw new Error("managed_move_destination_artifacts_changed");
|
|
4693
|
+
}
|
|
4694
|
+
if (managedArtifacts)
|
|
4695
|
+
managedSourceStore.removeTreeExpected(path.basename(oldArtifactDir), managedArtifacts);
|
|
4696
|
+
if (managedTranscript)
|
|
4697
|
+
managedSourceStore.removeExpected(path.basename(oldSessionFile), managedTranscript);
|
|
4698
|
+
} else {
|
|
4699
|
+
if (hadSessionFile && oldSessionFile !== newSessionFile) {
|
|
4700
|
+
await movePathAcrossDevicesSafe(oldSessionFile, newSessionFile);
|
|
4701
|
+
movedSessionFile = true;
|
|
4702
|
+
}
|
|
4703
|
+
try {
|
|
4704
|
+
const stat = await fs.promises.stat(oldArtifactDir);
|
|
4705
|
+
if (stat.isDirectory() && oldArtifactDir !== newArtifactDir) {
|
|
4706
|
+
await movePathAcrossDevicesSafe(oldArtifactDir, newArtifactDir);
|
|
4707
|
+
movedArtifactDir = true;
|
|
4708
|
+
}
|
|
4709
|
+
} catch (err) {
|
|
4710
|
+
if (!isEnoent(err)) throw err;
|
|
3583
4711
|
}
|
|
3584
4712
|
}
|
|
3585
|
-
|
|
4713
|
+
} catch (err) {
|
|
4714
|
+
if (managedMove && managedDestinationStore) {
|
|
3586
4715
|
try {
|
|
3587
|
-
|
|
4716
|
+
if (managedArtifacts && !managedPublishedArtifacts) {
|
|
4717
|
+
try {
|
|
4718
|
+
managedPublishedArtifacts = managedDestinationStore.captureTree(path.basename(newArtifactDir));
|
|
4719
|
+
} catch (captureError) {
|
|
4720
|
+
if (!(captureError instanceof Error) || captureError.message !== "not_found")
|
|
4721
|
+
throw captureError;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
if (managedSourceStore && managedArtifacts && managedPublishedArtifacts) {
|
|
4725
|
+
let sourceArtifacts: native.NativeDirectoryTreeSnapshot | undefined;
|
|
4726
|
+
try {
|
|
4727
|
+
sourceArtifacts = managedSourceStore.captureTree(path.basename(oldArtifactDir));
|
|
4728
|
+
} catch (captureError) {
|
|
4729
|
+
if (!(captureError instanceof Error) || captureError.message !== "not_found")
|
|
4730
|
+
throw captureError;
|
|
4731
|
+
}
|
|
4732
|
+
if (!sourceArtifacts || !retainedTreeSnapshotEquals(sourceArtifacts, managedArtifacts)) {
|
|
4733
|
+
if (sourceArtifacts)
|
|
4734
|
+
managedSourceStore.removeTreeExpected(path.basename(oldArtifactDir), sourceArtifacts);
|
|
4735
|
+
await copyManagedTree(
|
|
4736
|
+
managedDestinationStore,
|
|
4737
|
+
path.basename(newArtifactDir),
|
|
4738
|
+
managedSourceStore,
|
|
4739
|
+
path.basename(oldArtifactDir),
|
|
4740
|
+
managedPublishedArtifacts,
|
|
4741
|
+
);
|
|
4742
|
+
const restored = managedSourceStore.captureTree(path.basename(oldArtifactDir));
|
|
4743
|
+
if (!retainedTreeSnapshotEqualsAfterRename(restored, managedArtifacts))
|
|
4744
|
+
throw new Error("managed_move_source_restore_mismatch");
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
if (
|
|
4748
|
+
managedSourceStore &&
|
|
4749
|
+
managedTranscript &&
|
|
4750
|
+
!managedSourceStore.readExpected(path.basename(oldSessionFile))
|
|
4751
|
+
)
|
|
4752
|
+
await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
|
|
4753
|
+
if (managedPublishedArtifacts)
|
|
4754
|
+
managedDestinationStore.removeTreeExpected(
|
|
4755
|
+
path.basename(newArtifactDir),
|
|
4756
|
+
managedPublishedArtifacts,
|
|
4757
|
+
);
|
|
4758
|
+
if (managedPublishedTranscript)
|
|
4759
|
+
managedDestinationStore.removeExpected(path.basename(newSessionFile), managedPublishedTranscript);
|
|
3588
4760
|
} catch (rollbackErr) {
|
|
3589
4761
|
restoreResidentStateAndThrow(
|
|
3590
|
-
new Error(
|
|
3591
|
-
|
|
3592
|
-
),
|
|
4762
|
+
new Error(`Failed to rollback managed move: ${toError(rollbackErr).message}`, {
|
|
4763
|
+
cause: toError(err),
|
|
4764
|
+
}),
|
|
3593
4765
|
);
|
|
3594
4766
|
}
|
|
4767
|
+
} else {
|
|
4768
|
+
if (movedArtifactDir) await movePathAcrossDevicesSafe(newArtifactDir, oldArtifactDir);
|
|
4769
|
+
if (movedSessionFile) await movePathAcrossDevicesSafe(newSessionFile, oldSessionFile);
|
|
3595
4770
|
}
|
|
3596
4771
|
restoreResidentStateAndThrow(err);
|
|
3597
4772
|
}
|
|
@@ -3602,34 +4777,92 @@ export class SessionManager {
|
|
|
3602
4777
|
this.#bumpAllRevisions();
|
|
3603
4778
|
}
|
|
3604
4779
|
|
|
3605
|
-
// Update cwd and sessionDir after the move succeeds.
|
|
4780
|
+
// Update cwd and sessionDir after the physical move succeeds, but roll the move back if metadata persistence fails.
|
|
3606
4781
|
this.cwd = resolvedCwd;
|
|
3607
4782
|
this.sessionDir = newSessionDir;
|
|
4783
|
+
this.destination = nextDestination;
|
|
4784
|
+
this.#managedTranscriptStoreCache =
|
|
4785
|
+
managedMove && managedDestinationStore
|
|
4786
|
+
? { directory: path.resolve(newSessionDir), store: managedDestinationStore }
|
|
4787
|
+
: null;
|
|
3608
4788
|
|
|
3609
|
-
// Update the session header in fileEntries
|
|
3610
|
-
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
3611
|
-
if (header) {
|
|
3612
|
-
header.cwd = resolvedCwd;
|
|
3613
|
-
this.#headerExportRevision++;
|
|
3614
|
-
}
|
|
3615
|
-
|
|
3616
|
-
// Rewrite the session file at its new location with updated header.
|
|
3617
|
-
// hadSessionFile: file existed before move → must rewrite to update cwd
|
|
3618
|
-
// hasAssistant: assistant messages in memory but file missing → recreate from memory
|
|
3619
|
-
// Neither true → fresh session, never written → preserve lazy-persist
|
|
3620
4789
|
const hasAssistant = this.#fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
3621
|
-
|
|
3622
|
-
|
|
4790
|
+
try {
|
|
4791
|
+
if (this.persist && this.#sessionFile && hadSessionFile) {
|
|
4792
|
+
await this.#appendHeaderPatch({ cwd: resolvedCwd });
|
|
4793
|
+
await this.#rewriteFile();
|
|
4794
|
+
} else if (this.persist && this.#sessionFile && hasAssistant) {
|
|
4795
|
+
await this.#appendHeaderPatch({ cwd: resolvedCwd });
|
|
4796
|
+
await this.#rewriteFile();
|
|
4797
|
+
} else {
|
|
4798
|
+
await this.#appendHeaderPatch({ cwd: resolvedCwd });
|
|
4799
|
+
}
|
|
4800
|
+
} catch (error) {
|
|
4801
|
+
const failedSessionFile = this.#sessionFile;
|
|
4802
|
+
const failedArtifactDir = failedSessionFile?.slice(0, -6);
|
|
4803
|
+
try {
|
|
4804
|
+
await this.#closePersistWriter().catch(() => {});
|
|
4805
|
+
this.#persistChain = Promise.resolve();
|
|
4806
|
+
this.#persistError = undefined;
|
|
4807
|
+
this.#persistErrorReported = false;
|
|
4808
|
+
if (rollbackManagedMove) {
|
|
4809
|
+
await rollbackManagedMove();
|
|
4810
|
+
this.#sessionFile = previousSessionFile;
|
|
4811
|
+
this.cwd = previousCwd;
|
|
4812
|
+
this.sessionDir = previousSessionDir;
|
|
4813
|
+
this.destination = previousDestination;
|
|
4814
|
+
this.#managedTranscriptStoreCache =
|
|
4815
|
+
managedMove && managedSourceStore
|
|
4816
|
+
? { directory: path.resolve(previousSessionDir), store: managedSourceStore }
|
|
4817
|
+
: null;
|
|
4818
|
+
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
4819
|
+
if (header) applyHeaderPatch(header, { cwd: previousCwd });
|
|
4820
|
+
this.#headerExportRevision++;
|
|
4821
|
+
throw error;
|
|
4822
|
+
}
|
|
4823
|
+
if (
|
|
4824
|
+
this.storage instanceof FileSessionStorage &&
|
|
4825
|
+
failedArtifactDir &&
|
|
4826
|
+
previousArtifactDir &&
|
|
4827
|
+
failedArtifactDir !== previousArtifactDir &&
|
|
4828
|
+
fs.existsSync(failedArtifactDir)
|
|
4829
|
+
) {
|
|
4830
|
+
await movePathAcrossDevicesSafe(failedArtifactDir, previousArtifactDir);
|
|
4831
|
+
}
|
|
4832
|
+
if (
|
|
4833
|
+
this.storage instanceof FileSessionStorage &&
|
|
4834
|
+
failedSessionFile &&
|
|
4835
|
+
previousSessionFile &&
|
|
4836
|
+
failedSessionFile !== previousSessionFile &&
|
|
4837
|
+
fs.existsSync(failedSessionFile)
|
|
4838
|
+
) {
|
|
4839
|
+
await movePathAcrossDevicesSafe(failedSessionFile, previousSessionFile);
|
|
4840
|
+
}
|
|
4841
|
+
this.#sessionFile = previousSessionFile;
|
|
4842
|
+
this.cwd = previousCwd;
|
|
4843
|
+
this.sessionDir = previousSessionDir;
|
|
4844
|
+
this.destination = previousDestination;
|
|
4845
|
+
this.#managedTranscriptStoreCache = null;
|
|
4846
|
+
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
4847
|
+
if (header) applyHeaderPatch(header, { cwd: previousCwd });
|
|
4848
|
+
this.#headerExportRevision++;
|
|
4849
|
+
if (previousSessionFile && this.storage.existsSync(previousSessionFile)) await this.#rewriteFile();
|
|
4850
|
+
} catch (rollbackError) {
|
|
4851
|
+
throw new Error(`Failed to rollback session move: ${toError(rollbackError).message}`, {
|
|
4852
|
+
cause: toError(error),
|
|
4853
|
+
});
|
|
4854
|
+
}
|
|
4855
|
+
throw error;
|
|
3623
4856
|
}
|
|
3624
4857
|
|
|
3625
|
-
// Update terminal breadcrumb
|
|
4858
|
+
// Update terminal breadcrumb only after the durable cwd transition succeeds.
|
|
3626
4859
|
if (this.#sessionFile) {
|
|
3627
4860
|
writeTerminalBreadcrumb(resolvedCwd, this.#sessionFile);
|
|
3628
4861
|
}
|
|
3629
4862
|
}
|
|
3630
4863
|
|
|
3631
4864
|
/** Sync version for initial creation (no existing writer to close) */
|
|
3632
|
-
#newSessionSync(options?: NewSessionOptions): string | undefined {
|
|
4865
|
+
#newSessionSync(options?: NewSessionOptions, writeBreadcrumb = true): string | undefined {
|
|
3633
4866
|
this.#persistChain = Promise.resolve();
|
|
3634
4867
|
this.#persistError = undefined;
|
|
3635
4868
|
this.#persistErrorReported = false;
|
|
@@ -3660,11 +4893,14 @@ export class SessionManager {
|
|
|
3660
4893
|
this.#usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, premiumRequests: 0, cost: 0 };
|
|
3661
4894
|
this.#inMemoryArtifacts = null;
|
|
3662
4895
|
this.#inMemoryArtifactCounter = 0;
|
|
4896
|
+
this.#artifactManager = null;
|
|
4897
|
+
this.#artifactManagerSessionFile = null;
|
|
4898
|
+
this.#adoptedArtifactManager = null;
|
|
3663
4899
|
|
|
3664
4900
|
if (this.persist) {
|
|
3665
4901
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
3666
4902
|
this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
|
|
3667
|
-
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
4903
|
+
if (writeBreadcrumb) writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
3668
4904
|
}
|
|
3669
4905
|
this.#resetResidentTextBlobStore();
|
|
3670
4906
|
return this.#sessionFile;
|
|
@@ -3738,6 +4974,10 @@ export class SessionManager {
|
|
|
3738
4974
|
#ensurePersistWriter(): NdjsonFileWriter | undefined {
|
|
3739
4975
|
if (!this.persist || !this.#sessionFile) return undefined;
|
|
3740
4976
|
if (this.#persistError) throw this.#persistError;
|
|
4977
|
+
if (this.destination.kind === "managed") {
|
|
4978
|
+
this.#managedTranscriptStore();
|
|
4979
|
+
return undefined;
|
|
4980
|
+
}
|
|
3741
4981
|
if (this.#persistWriter && this.#persistWriterPath === this.#sessionFile) {
|
|
3742
4982
|
if (this.#persistWriter.isOpen()) return this.#persistWriter;
|
|
3743
4983
|
// Cached writer for the current file is mid-close (queued
|
|
@@ -3788,7 +5028,11 @@ export class SessionManager {
|
|
|
3788
5028
|
// shared `*.bak` glob on both real and in-memory storage backends and promote it back to
|
|
3789
5029
|
// the primary on the next session-dir scan.
|
|
3790
5030
|
|
|
3791
|
-
#replaceSessionFileAfterEpermSync(
|
|
5031
|
+
#replaceSessionFileAfterEpermSync(
|
|
5032
|
+
tempPath: string,
|
|
5033
|
+
targetPath: string,
|
|
5034
|
+
renameError: unknown,
|
|
5035
|
+
): SessionFileReplacementSyncOutcome {
|
|
3792
5036
|
const dir = path.resolve(targetPath, "..");
|
|
3793
5037
|
const backupPath = path.join(dir, `${path.basename(targetPath)}.${Snowflake.next()}.bak`);
|
|
3794
5038
|
try {
|
|
@@ -3796,7 +5040,7 @@ export class SessionManager {
|
|
|
3796
5040
|
} catch (err) {
|
|
3797
5041
|
if (isEnoent(err)) {
|
|
3798
5042
|
this.storage.renameSync(tempPath, targetPath);
|
|
3799
|
-
return;
|
|
5043
|
+
return { kind: "replaced" };
|
|
3800
5044
|
}
|
|
3801
5045
|
throw toError(renameError);
|
|
3802
5046
|
}
|
|
@@ -3815,7 +5059,7 @@ export class SessionManager {
|
|
|
3815
5059
|
{ cause: originalError },
|
|
3816
5060
|
);
|
|
3817
5061
|
}
|
|
3818
|
-
|
|
5062
|
+
return { kind: "restored_previous", error: replaceError };
|
|
3819
5063
|
}
|
|
3820
5064
|
|
|
3821
5065
|
try {
|
|
@@ -3829,6 +5073,7 @@ export class SessionManager {
|
|
|
3829
5073
|
});
|
|
3830
5074
|
}
|
|
3831
5075
|
}
|
|
5076
|
+
return { kind: "replaced" };
|
|
3832
5077
|
}
|
|
3833
5078
|
|
|
3834
5079
|
async #replaceSessionFileAfterEperm(tempPath: string, targetPath: string, renameError: unknown): Promise<void> {
|
|
@@ -3883,21 +5128,44 @@ export class SessionManager {
|
|
|
3883
5128
|
}
|
|
3884
5129
|
}
|
|
3885
5130
|
|
|
3886
|
-
#replaceSessionFileSync(tempPath: string, targetPath: string):
|
|
5131
|
+
#replaceSessionFileSync(tempPath: string, targetPath: string): SessionFileReplacementSyncOutcome {
|
|
3887
5132
|
try {
|
|
3888
5133
|
this.storage.renameSync(tempPath, targetPath);
|
|
5134
|
+
return { kind: "replaced" };
|
|
3889
5135
|
} catch (err) {
|
|
3890
5136
|
if (hasFsCode(err, "EPERM")) {
|
|
3891
|
-
this.#replaceSessionFileAfterEpermSync(tempPath, targetPath, err);
|
|
3892
|
-
return;
|
|
5137
|
+
return this.#replaceSessionFileAfterEpermSync(tempPath, targetPath, err);
|
|
3893
5138
|
}
|
|
3894
5139
|
|
|
3895
5140
|
throw toError(err);
|
|
3896
5141
|
}
|
|
3897
5142
|
}
|
|
3898
5143
|
|
|
5144
|
+
#managedTranscriptStore(sessionFile = this.#sessionFile): ManagedSessionDescendantStore {
|
|
5145
|
+
if (this.destination.kind !== "managed" || !sessionFile) {
|
|
5146
|
+
throw new Error("Managed transcript authority is unavailable");
|
|
5147
|
+
}
|
|
5148
|
+
const sessionDir = path.resolve(path.dirname(sessionFile));
|
|
5149
|
+
if (sessionDir !== path.resolve(this.destination.directory)) {
|
|
5150
|
+
throw new Error("Managed transcript escaped its session directory");
|
|
5151
|
+
}
|
|
5152
|
+
if (this.#managedTranscriptStoreCache) {
|
|
5153
|
+
if (this.#managedTranscriptStoreCache.directory !== sessionDir)
|
|
5154
|
+
throw new Error("Managed transcript store transition was not verified");
|
|
5155
|
+
return this.#managedTranscriptStoreCache.store;
|
|
5156
|
+
}
|
|
5157
|
+
const store = managedStoreFromContext(this.destination.securityContext, sessionDir);
|
|
5158
|
+
this.#managedTranscriptStoreCache = { directory: sessionDir, store };
|
|
5159
|
+
return store;
|
|
5160
|
+
}
|
|
5161
|
+
|
|
3899
5162
|
#writeEntriesAtomicallySync(entries: FileEntry[]): void {
|
|
3900
5163
|
if (!this.#sessionFile) return;
|
|
5164
|
+
if (this.destination.kind === "managed") {
|
|
5165
|
+
const bytes = Buffer.from(`${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`, "utf8");
|
|
5166
|
+
this.#managedTranscriptStore().replaceSync(path.basename(this.#sessionFile), bytes);
|
|
5167
|
+
return;
|
|
5168
|
+
}
|
|
3901
5169
|
const dir = path.resolve(this.#sessionFile, "..");
|
|
3902
5170
|
const tempPath = path.join(dir, `.${path.basename(this.#sessionFile)}.${Snowflake.next()}.tmp`);
|
|
3903
5171
|
const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
|
|
@@ -3906,7 +5174,8 @@ export class SessionManager {
|
|
|
3906
5174
|
writer.writeSync(entry);
|
|
3907
5175
|
}
|
|
3908
5176
|
writer.closeSync();
|
|
3909
|
-
this.#replaceSessionFileSync(tempPath, this.#sessionFile);
|
|
5177
|
+
const replacement = this.#replaceSessionFileSync(tempPath, this.#sessionFile);
|
|
5178
|
+
if (replacement.kind === "restored_previous") throw replacement.error;
|
|
3910
5179
|
} catch (err) {
|
|
3911
5180
|
// closeSync is now truthful and may throw; wrap the best-effort cleanup so
|
|
3912
5181
|
// the original error (write/close failure) is the one surfaced, not the
|
|
@@ -3923,6 +5192,11 @@ export class SessionManager {
|
|
|
3923
5192
|
}
|
|
3924
5193
|
async #writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
|
|
3925
5194
|
if (!this.#sessionFile) return;
|
|
5195
|
+
if (this.destination.kind === "managed") {
|
|
5196
|
+
const bytes = Buffer.from(`${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`, "utf8");
|
|
5197
|
+
await this.#managedTranscriptStore().replace(path.basename(this.#sessionFile), bytes);
|
|
5198
|
+
return;
|
|
5199
|
+
}
|
|
3926
5200
|
const dir = path.resolve(this.#sessionFile, "..");
|
|
3927
5201
|
const tempPath = path.join(dir, `.${path.basename(this.#sessionFile)}.${Snowflake.next()}.tmp`);
|
|
3928
5202
|
const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
|
|
@@ -3949,19 +5223,58 @@ export class SessionManager {
|
|
|
3949
5223
|
}
|
|
3950
5224
|
}
|
|
3951
5225
|
|
|
3952
|
-
async #
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
5226
|
+
async #writeStagedDefaultModelSelection(
|
|
5227
|
+
entries: readonly FileEntry[],
|
|
5228
|
+
sessionFile: string,
|
|
5229
|
+
): Promise<string | undefined> {
|
|
5230
|
+
if (this.destination.kind === "managed") return undefined;
|
|
5231
|
+
if (!this.storage.existsSync(sessionFile)) return undefined;
|
|
5232
|
+
const dir = path.resolve(sessionFile, "..");
|
|
5233
|
+
const tempPath = path.join(dir, `.${path.basename(sessionFile)}.${Snowflake.next()}.default-selection.tmp`);
|
|
5234
|
+
const writer = new NdjsonFileWriter(this.storage, tempPath, {
|
|
5235
|
+
flags: "w",
|
|
5236
|
+
});
|
|
5237
|
+
try {
|
|
5238
|
+
const persistedEntries = await Promise.all(
|
|
5239
|
+
materializeResidentEntriesForPersistenceSync([...entries], this.#residentBlobStores()).map(entry =>
|
|
3958
5240
|
prepareEntryForPersistence(entry, this.#blobStore),
|
|
3959
5241
|
),
|
|
3960
5242
|
);
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
5243
|
+
for (const entry of persistedEntries) {
|
|
5244
|
+
await writer.write(entry);
|
|
5245
|
+
}
|
|
5246
|
+
await writer.flush();
|
|
5247
|
+
await writer.fsync();
|
|
5248
|
+
await writer.close();
|
|
5249
|
+
return tempPath;
|
|
5250
|
+
} catch (error) {
|
|
5251
|
+
try {
|
|
5252
|
+
await writer.close();
|
|
5253
|
+
} catch {}
|
|
5254
|
+
try {
|
|
5255
|
+
await this.storage.unlink(tempPath);
|
|
5256
|
+
} catch {}
|
|
5257
|
+
throw toError(error);
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5261
|
+
async #rewriteFileContents(): Promise<void> {
|
|
5262
|
+
if (!this.persist || !this.#sessionFile) return;
|
|
5263
|
+
await this.#closePersistWriterInternal();
|
|
5264
|
+
const entries = await Promise.all(
|
|
5265
|
+
materializeResidentEntriesForPersistenceSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
|
|
5266
|
+
prepareEntryForPersistence(entry, this.#blobStore),
|
|
5267
|
+
),
|
|
5268
|
+
);
|
|
5269
|
+
await this.#writeEntriesAtomically(entries);
|
|
5270
|
+
this.#needsFullRewriteOnNextPersist = false;
|
|
5271
|
+
this.#flushed = true;
|
|
5272
|
+
this.#ensuredOnDisk = true;
|
|
5273
|
+
}
|
|
5274
|
+
|
|
5275
|
+
async #rewriteFile(): Promise<void> {
|
|
5276
|
+
await this.#queuePersistTask(async () => {
|
|
5277
|
+
await this.#rewriteFileContents();
|
|
3965
5278
|
});
|
|
3966
5279
|
}
|
|
3967
5280
|
|
|
@@ -3981,6 +5294,121 @@ export class SessionManager {
|
|
|
3981
5294
|
return this.persist;
|
|
3982
5295
|
}
|
|
3983
5296
|
|
|
5297
|
+
async stageDefaultModelSelection(
|
|
5298
|
+
model: string,
|
|
5299
|
+
thinkingLevel: string | undefined,
|
|
5300
|
+
options?: { readonly appendThinkingLevel: boolean },
|
|
5301
|
+
): Promise<DefaultModelSelectionStage> {
|
|
5302
|
+
const entryRevision = this.#entryRevision;
|
|
5303
|
+
const headerExportRevision = this.#headerExportRevision;
|
|
5304
|
+
const sessionId = this.#sessionId;
|
|
5305
|
+
const sessionFile = this.#sessionFile;
|
|
5306
|
+
const entryIds = new Map(this.#byId);
|
|
5307
|
+
let parentId = this.#leafId;
|
|
5308
|
+
const entries = [...this.#fileEntries];
|
|
5309
|
+
const temporaryEntry: ModelChangeEntry = {
|
|
5310
|
+
type: "model_change",
|
|
5311
|
+
id: generateId(entryIds),
|
|
5312
|
+
parentId,
|
|
5313
|
+
timestamp: new Date().toISOString(),
|
|
5314
|
+
model,
|
|
5315
|
+
role: "temporary",
|
|
5316
|
+
};
|
|
5317
|
+
entryIds.set(temporaryEntry.id, temporaryEntry);
|
|
5318
|
+
entries.push(temporaryEntry);
|
|
5319
|
+
parentId = temporaryEntry.id;
|
|
5320
|
+
if (options?.appendThinkingLevel) {
|
|
5321
|
+
const thinkingEntry: ThinkingLevelChangeEntry = {
|
|
5322
|
+
type: "thinking_level_change",
|
|
5323
|
+
id: generateId(entryIds),
|
|
5324
|
+
parentId,
|
|
5325
|
+
timestamp: new Date().toISOString(),
|
|
5326
|
+
thinkingLevel: thinkingLevel ?? null,
|
|
5327
|
+
};
|
|
5328
|
+
entryIds.set(thinkingEntry.id, thinkingEntry);
|
|
5329
|
+
entries.push(thinkingEntry);
|
|
5330
|
+
parentId = thinkingEntry.id;
|
|
5331
|
+
}
|
|
5332
|
+
const modelEntry: ModelChangeEntry = {
|
|
5333
|
+
type: "model_change",
|
|
5334
|
+
id: generateId(entryIds),
|
|
5335
|
+
parentId,
|
|
5336
|
+
timestamp: new Date().toISOString(),
|
|
5337
|
+
model,
|
|
5338
|
+
role: "default",
|
|
5339
|
+
};
|
|
5340
|
+
entries.push(modelEntry);
|
|
5341
|
+
const persistsToExistingFile = this.persist && sessionFile !== undefined && this.storage.existsSync(sessionFile);
|
|
5342
|
+
const tempPath = persistsToExistingFile
|
|
5343
|
+
? await this.#writeStagedDefaultModelSelection(entries, sessionFile)
|
|
5344
|
+
: undefined;
|
|
5345
|
+
return {
|
|
5346
|
+
entryRevision,
|
|
5347
|
+
headerExportRevision,
|
|
5348
|
+
sessionId,
|
|
5349
|
+
sessionFile,
|
|
5350
|
+
entries,
|
|
5351
|
+
tempPath,
|
|
5352
|
+
persistsToExistingFile,
|
|
5353
|
+
};
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
promoteDefaultModelSelection(stage: DefaultModelSelectionStage): DefaultModelSelectionPromotion {
|
|
5357
|
+
if (
|
|
5358
|
+
stage.entryRevision !== this.#entryRevision ||
|
|
5359
|
+
stage.headerExportRevision !== this.#headerExportRevision ||
|
|
5360
|
+
stage.sessionId !== this.#sessionId ||
|
|
5361
|
+
stage.sessionFile !== this.#sessionFile
|
|
5362
|
+
) {
|
|
5363
|
+
return { kind: "not_promoted" };
|
|
5364
|
+
}
|
|
5365
|
+
if (stage.persistsToExistingFile) {
|
|
5366
|
+
if (!this.#sessionFile) return { kind: "unknown", error: new Error("Missing staged session replacement") };
|
|
5367
|
+
if (this.destination.kind === "managed") {
|
|
5368
|
+
try {
|
|
5369
|
+
const persistedEntries = materializeResidentEntriesForPersistenceSync(
|
|
5370
|
+
[...stage.entries],
|
|
5371
|
+
this.#residentBlobStores(),
|
|
5372
|
+
).map(entry => prepareEntryForPersistenceSync(entry, this.#blobStore));
|
|
5373
|
+
this.#writeEntriesAtomicallySync(persistedEntries);
|
|
5374
|
+
} catch (error) {
|
|
5375
|
+
return { kind: "unknown", error: toError(error) };
|
|
5376
|
+
}
|
|
5377
|
+
} else {
|
|
5378
|
+
if (!stage.tempPath) return { kind: "unknown", error: new Error("Missing staged session replacement") };
|
|
5379
|
+
try {
|
|
5380
|
+
this.#closePersistWriterInternalSync();
|
|
5381
|
+
const replacement = this.#replaceSessionFileSync(stage.tempPath, this.#sessionFile);
|
|
5382
|
+
if (replacement.kind === "restored_previous") {
|
|
5383
|
+
return { kind: "not_promoted", error: replacement.error };
|
|
5384
|
+
}
|
|
5385
|
+
} catch (error) {
|
|
5386
|
+
if (this.#persistWriter?.getCloseState() === "close_failed_retryable") {
|
|
5387
|
+
return { kind: "not_promoted", error: new Error("Session replacement could not be completed.") };
|
|
5388
|
+
}
|
|
5389
|
+
return { kind: "unknown", error: toError(error) };
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
this.#fileEntries = [...stage.entries];
|
|
5394
|
+
this.#resetResidentTextBlobStore();
|
|
5395
|
+
this.#reexternalizeFileEntriesForResidentStore();
|
|
5396
|
+
this.#needsFullRewriteOnNextPersist = false;
|
|
5397
|
+
this.#flushed = stage.persistsToExistingFile;
|
|
5398
|
+
this.#ensuredOnDisk = stage.persistsToExistingFile;
|
|
5399
|
+
this.#bumpAllRevisions();
|
|
5400
|
+
return { kind: "promoted" };
|
|
5401
|
+
}
|
|
5402
|
+
|
|
5403
|
+
async discardDefaultModelSelectionStage(stage: DefaultModelSelectionStage): Promise<void> {
|
|
5404
|
+
if (!stage.tempPath) return;
|
|
5405
|
+
try {
|
|
5406
|
+
await this.storage.unlink(stage.tempPath);
|
|
5407
|
+
} catch (error) {
|
|
5408
|
+
if (!isEnoent(error)) throw toError(error);
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
|
|
3984
5412
|
/**
|
|
3985
5413
|
* Force-persist all current entries to disk, even when no assistant message exists yet.
|
|
3986
5414
|
* Used by ACP mode where session/new must create a discoverable session immediately.
|
|
@@ -4107,15 +5535,62 @@ export class SessionManager {
|
|
|
4107
5535
|
* Returns the session artifacts directory path (session file path without .jsonl).
|
|
4108
5536
|
* Returns null when the session is not persisted to a file.
|
|
4109
5537
|
* When this session has adopted an external ArtifactManager (subagent case),
|
|
4110
|
-
*
|
|
4111
|
-
*
|
|
5538
|
+
* never exposes that managed directory as a pathname. Reads and writes use the
|
|
5539
|
+
* adopted manager capability directly.
|
|
4112
5540
|
*/
|
|
4113
5541
|
getArtifactsDir(): string | null {
|
|
4114
|
-
if (this.#adoptedArtifactManager) return
|
|
5542
|
+
if (this.#adoptedArtifactManager) return null;
|
|
4115
5543
|
const sessionFile = this.#sessionFile;
|
|
4116
5544
|
return sessionFile ? sessionFile.slice(0, -6) : null;
|
|
4117
5545
|
}
|
|
4118
5546
|
|
|
5547
|
+
isManagedDestination(): boolean {
|
|
5548
|
+
return this.destination.kind === "managed";
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5551
|
+
/** Supplies opaque retained authority for mandatory managed legacy local migration. */
|
|
5552
|
+
getManagedLegacyLocalMigrationSource(): ManagedLegacyLocalMigrationSource | null {
|
|
5553
|
+
if (this.destination.kind !== "managed" || !this.#sessionFile || this.#adoptedArtifactManager) return null;
|
|
5554
|
+
const store = this.#managedTranscriptStore();
|
|
5555
|
+
return {
|
|
5556
|
+
capture: async () => {
|
|
5557
|
+
let snapshot: native.NativeDirectoryTreeSnapshot;
|
|
5558
|
+
try {
|
|
5559
|
+
snapshot = store.captureTree("local");
|
|
5560
|
+
} catch (error) {
|
|
5561
|
+
if (error instanceof Error && error.message === "not_found") return null;
|
|
5562
|
+
throw error;
|
|
5563
|
+
}
|
|
5564
|
+
let totalBytes = 0;
|
|
5565
|
+
for (const entry of snapshot.entries) if (entry.kind === "file") totalBytes += Number(entry.size);
|
|
5566
|
+
if (totalBytes > 64 * 1024 * 1024) throw new Error("Legacy local:// migration exceeds the safe size limit");
|
|
5567
|
+
const entries = snapshot.entries.map(entry => {
|
|
5568
|
+
if (entry.kind === "directory") return { relativePath: entry.relativePath, kind: "directory" as const };
|
|
5569
|
+
const captured = store.readExpected(path.posix.join("local", entry.relativePath));
|
|
5570
|
+
if (
|
|
5571
|
+
!captured ||
|
|
5572
|
+
captured.identity.dev.toString() !== entry.dev ||
|
|
5573
|
+
captured.identity.ino.toString() !== entry.ino ||
|
|
5574
|
+
captured.identity.size.toString() !== entry.size ||
|
|
5575
|
+
crypto.createHash("sha256").update(captured.bytes).digest("hex") !== entry.sha256
|
|
5576
|
+
)
|
|
5577
|
+
throw new Error("Legacy local:// migration source changed during capture");
|
|
5578
|
+
return {
|
|
5579
|
+
relativePath: entry.relativePath,
|
|
5580
|
+
kind: "file" as const,
|
|
5581
|
+
bytes: captured.bytes,
|
|
5582
|
+
sha256: entry.sha256,
|
|
5583
|
+
};
|
|
5584
|
+
});
|
|
5585
|
+
const verified = store.captureTree("local");
|
|
5586
|
+
if (JSON.stringify(verified) !== JSON.stringify(snapshot))
|
|
5587
|
+
throw new Error("Legacy local:// migration source changed during capture");
|
|
5588
|
+
return { snapshot, entries };
|
|
5589
|
+
},
|
|
5590
|
+
retire: snapshot => store.removeTreeExpected("local", snapshot),
|
|
5591
|
+
};
|
|
5592
|
+
}
|
|
5593
|
+
|
|
4119
5594
|
/**
|
|
4120
5595
|
* Adopt an externally-owned ArtifactManager. Used by subagents to share
|
|
4121
5596
|
* the parent session's artifact directory and ID counter.
|
|
@@ -4151,7 +5626,12 @@ export class SessionManager {
|
|
|
4151
5626
|
return this.#artifactManager;
|
|
4152
5627
|
}
|
|
4153
5628
|
|
|
4154
|
-
const
|
|
5629
|
+
const artifactDir = sessionFile.slice(0, -6);
|
|
5630
|
+
let artifactStorage: string | ManagedSessionDescendantStore = artifactDir;
|
|
5631
|
+
if (this.destination.kind === "managed") {
|
|
5632
|
+
artifactStorage = this.#managedTranscriptStore(sessionFile).deriveSubtree(path.basename(artifactDir));
|
|
5633
|
+
}
|
|
5634
|
+
const manager = new ArtifactManager(artifactStorage);
|
|
4155
5635
|
this.#artifactManager = manager;
|
|
4156
5636
|
this.#artifactManagerSessionFile = sessionFile;
|
|
4157
5637
|
return manager;
|
|
@@ -4192,21 +5672,43 @@ export class SessionManager {
|
|
|
4192
5672
|
}
|
|
4193
5673
|
|
|
4194
5674
|
/**
|
|
4195
|
-
* Path to the unsent-input draft sidecar for
|
|
4196
|
-
* the artifacts directory so
|
|
4197
|
-
*
|
|
5675
|
+
* Path to the unsent-input draft sidecar for an explicit session. It lives
|
|
5676
|
+
* inside the artifacts directory so `dropSession` removes it with its owner.
|
|
5677
|
+
* Managed drafts use a retained descendant-store capability instead.
|
|
4198
5678
|
*/
|
|
4199
5679
|
#getDraftPath(): string | null {
|
|
5680
|
+
if (this.destination.kind === "managed") return null;
|
|
4200
5681
|
const dir = this.getArtifactsDir();
|
|
4201
5682
|
return dir ? path.join(dir, "draft.txt") : null;
|
|
4202
5683
|
}
|
|
4203
5684
|
|
|
5685
|
+
/** Retained authority for this managed session's private draft sidecar. */
|
|
5686
|
+
#getManagedDraftStore(): ManagedSessionDescendantStore | null {
|
|
5687
|
+
if (this.destination.kind !== "managed" || !this.#sessionFile) return null;
|
|
5688
|
+
return this.#managedTranscriptStore().deriveSubtree(path.basename(this.#sessionFile.slice(0, -6)));
|
|
5689
|
+
}
|
|
5690
|
+
|
|
4204
5691
|
/**
|
|
4205
5692
|
* Persist (or clear) the current editor draft so the next resume of this
|
|
4206
5693
|
* session can restore it. Empty text deletes any stale draft. No-op when the
|
|
4207
5694
|
* session is not persisted.
|
|
4208
5695
|
*/
|
|
4209
5696
|
async saveDraft(text: string): Promise<void> {
|
|
5697
|
+
if (this.destination.kind === "managed") {
|
|
5698
|
+
if (text.length === 0) {
|
|
5699
|
+
await this.#getManagedDraftStore()?.remove("draft.txt");
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5702
|
+
// Force the session header onto disk so resume can find the file we are
|
|
5703
|
+
// attaching this draft to. Without this, a session whose first message
|
|
5704
|
+
// never produced an assistant reply would persist a draft next to a
|
|
5705
|
+
// session file that does not exist on disk.
|
|
5706
|
+
await this.ensureOnDisk();
|
|
5707
|
+
const store = this.#getManagedDraftStore();
|
|
5708
|
+
if (!store) return;
|
|
5709
|
+
await store.replace("draft.txt", Buffer.from(text, "utf8"));
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
4210
5712
|
const draftPath = this.#getDraftPath();
|
|
4211
5713
|
if (!draftPath || !this.persist) return;
|
|
4212
5714
|
if (text.length === 0) {
|
|
@@ -4222,7 +5724,9 @@ export class SessionManager {
|
|
|
4222
5724
|
// never produced an assistant reply would persist a draft next to a
|
|
4223
5725
|
// session file that does not exist on disk.
|
|
4224
5726
|
await this.ensureOnDisk();
|
|
4225
|
-
|
|
5727
|
+
const artifactManager = this.#getOrCreateArtifactManager();
|
|
5728
|
+
if (!artifactManager) return;
|
|
5729
|
+
await artifactManager.replaceNamed("draft.txt", text);
|
|
4226
5730
|
}
|
|
4227
5731
|
|
|
4228
5732
|
/**
|
|
@@ -4231,6 +5735,10 @@ export class SessionManager {
|
|
|
4231
5735
|
* sidecar so a subsequent resume does not re-restore the same text.
|
|
4232
5736
|
*/
|
|
4233
5737
|
async consumeDraft(): Promise<string | null> {
|
|
5738
|
+
if (this.destination.kind === "managed") {
|
|
5739
|
+
const draft = await this.#getManagedDraftStore()?.consume("draft.txt");
|
|
5740
|
+
return draft ? Buffer.from(draft).toString("utf8") : null;
|
|
5741
|
+
}
|
|
4234
5742
|
const draftPath = this.#getDraftPath();
|
|
4235
5743
|
if (!draftPath) return null;
|
|
4236
5744
|
let text: string;
|
|
@@ -4262,7 +5770,8 @@ export class SessionManager {
|
|
|
4262
5770
|
return name
|
|
4263
5771
|
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
4264
5772
|
.replace(/ +/g, " ")
|
|
4265
|
-
.trim()
|
|
5773
|
+
.trim()
|
|
5774
|
+
.slice(0, SESSION_NAME_MAX_CHARS);
|
|
4266
5775
|
}
|
|
4267
5776
|
|
|
4268
5777
|
/**
|
|
@@ -4279,23 +5788,51 @@ export class SessionManager {
|
|
|
4279
5788
|
|
|
4280
5789
|
this.#sessionName = sanitized;
|
|
4281
5790
|
this.#titleSource = source;
|
|
5791
|
+
await this.#appendHeaderPatch({ title: sanitized, titleSource: source });
|
|
5792
|
+
return true;
|
|
5793
|
+
}
|
|
4282
5794
|
|
|
4283
|
-
|
|
4284
|
-
const header = this.#fileEntries.find(
|
|
4285
|
-
if (header)
|
|
4286
|
-
|
|
4287
|
-
header.titleSource = source;
|
|
4288
|
-
}
|
|
5795
|
+
async #appendHeaderPatch(patch: HeaderPatchRecord["patch"]): Promise<void> {
|
|
5796
|
+
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
5797
|
+
if (!header) return;
|
|
5798
|
+
applyHeaderPatch(header, patch);
|
|
4289
5799
|
this.#headerExportRevision++;
|
|
5800
|
+
await this.#persistPatch({ type: "header_patch", patch });
|
|
5801
|
+
}
|
|
4290
5802
|
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
}
|
|
4296
|
-
return true;
|
|
5803
|
+
#appendManagedRecordSync(record: FileEntry | SessionPatchRecord): void {
|
|
5804
|
+
if (!this.#sessionFile) throw new Error("Managed transcript path is unavailable");
|
|
5805
|
+
const store = this.#managedTranscriptStore();
|
|
5806
|
+
const relativePath = path.basename(this.#sessionFile);
|
|
5807
|
+
store.appendSync(relativePath, Buffer.from(`${JSON.stringify(record)}\n`, "utf8"));
|
|
4297
5808
|
}
|
|
4298
5809
|
|
|
5810
|
+
async #persistPatch(record: SessionPatchRecord): Promise<void> {
|
|
5811
|
+
if (!this.persist || !this.#sessionFile || !this.storage.existsSync(this.#sessionFile)) return;
|
|
5812
|
+
await this.#queuePersistTask(async () => {
|
|
5813
|
+
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
5814
|
+
if (this.#needsFullRewriteOnNextPersist || !this.#flushed || (header?.version ?? 1) < CURRENT_SESSION_VERSION)
|
|
5815
|
+
return this.#rewriteFileContents();
|
|
5816
|
+
const persistedRecord =
|
|
5817
|
+
record.type === "entry_patch"
|
|
5818
|
+
? (prepareEntryForPersistenceSync(
|
|
5819
|
+
materializeResidentEntryForPersistenceSync(
|
|
5820
|
+
record as unknown as FileEntry,
|
|
5821
|
+
this.#residentBlobStores(),
|
|
5822
|
+
new Map(),
|
|
5823
|
+
),
|
|
5824
|
+
this.#blobStore,
|
|
5825
|
+
) as unknown as SessionPatchRecord)
|
|
5826
|
+
: record;
|
|
5827
|
+
if (this.destination.kind === "managed") {
|
|
5828
|
+
this.#appendManagedRecordSync(persistedRecord);
|
|
5829
|
+
return;
|
|
5830
|
+
}
|
|
5831
|
+
const writer = this.#ensurePersistWriter();
|
|
5832
|
+
if (!writer) return this.#rewriteFileContents();
|
|
5833
|
+
await writer.write(persistedRecord);
|
|
5834
|
+
});
|
|
5835
|
+
}
|
|
4299
5836
|
_persist(entry: SessionEntry): void {
|
|
4300
5837
|
if (!this.persist || !this.#sessionFile) return;
|
|
4301
5838
|
if (this.#persistError) throw this.#persistError;
|
|
@@ -4308,6 +5845,7 @@ export class SessionManager {
|
|
|
4308
5845
|
if (!hasAssistant) {
|
|
4309
5846
|
// Mark as not flushed so when assistant arrives, all entries get written.
|
|
4310
5847
|
this.#flushed = false;
|
|
5848
|
+
|
|
4311
5849
|
this.#ensuredOnDisk = false;
|
|
4312
5850
|
return;
|
|
4313
5851
|
}
|
|
@@ -4334,6 +5872,16 @@ export class SessionManager {
|
|
|
4334
5872
|
// the synchronous blob-store path so blob bytes are durable before the JSONL
|
|
4335
5873
|
// line referencing them is written.
|
|
4336
5874
|
try {
|
|
5875
|
+
if (this.destination.kind === "managed") {
|
|
5876
|
+
const materializedEntry = materializeResidentEntryForPersistenceSync(
|
|
5877
|
+
entry,
|
|
5878
|
+
this.#residentBlobStores(),
|
|
5879
|
+
new Map(),
|
|
5880
|
+
);
|
|
5881
|
+
const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
|
|
5882
|
+
this.#appendManagedRecordSync(persistedEntry);
|
|
5883
|
+
return;
|
|
5884
|
+
}
|
|
4337
5885
|
const writer = this.#ensurePersistWriter();
|
|
4338
5886
|
if (!writer) {
|
|
4339
5887
|
// `#ensurePersistWriter` returns undefined here only when the cached
|
|
@@ -4389,6 +5937,24 @@ export class SessionManager {
|
|
|
4389
5937
|
}
|
|
4390
5938
|
}
|
|
4391
5939
|
|
|
5940
|
+
/** Append a configured fallback chain as child of current leaf, then advance leaf. Returns entry id. */
|
|
5941
|
+
appendConfiguredModelChain(chain: ConfiguredModelChain): string {
|
|
5942
|
+
const entry: ConfiguredModelChainEntry = {
|
|
5943
|
+
type: "configured_model_chain",
|
|
5944
|
+
id: generateId(this.#byId),
|
|
5945
|
+
parentId: this.#leafId,
|
|
5946
|
+
timestamp: new Date().toISOString(),
|
|
5947
|
+
role: chain.role,
|
|
5948
|
+
entries: [...chain.entries],
|
|
5949
|
+
origin: chain.origin,
|
|
5950
|
+
identity: chain.identity,
|
|
5951
|
+
explicitHead: chain.explicitHead,
|
|
5952
|
+
cleared: chain.cleared,
|
|
5953
|
+
};
|
|
5954
|
+
this.#appendEntry(entry);
|
|
5955
|
+
return entry.id;
|
|
5956
|
+
}
|
|
5957
|
+
|
|
4392
5958
|
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
|
|
4393
5959
|
* Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
|
|
4394
5960
|
* Reason: we want these to be top-level entries in the session, not message session entries,
|
|
@@ -4583,6 +6149,22 @@ export class SessionManager {
|
|
|
4583
6149
|
this.#replayMetadataRevision++;
|
|
4584
6150
|
}
|
|
4585
6151
|
|
|
6152
|
+
/** Write mutated custom-message entries back into the canonical entry store by id. */
|
|
6153
|
+
applyCustomMessageEntryUpdates(entries: readonly CustomMessageEntry[]): void {
|
|
6154
|
+
for (const updated of entries) {
|
|
6155
|
+
const canonical = this.#byId.get(updated.id);
|
|
6156
|
+
if (canonical?.type !== "custom_message") continue;
|
|
6157
|
+
canonical.content = updated.content;
|
|
6158
|
+
canonical.details = updated.details;
|
|
6159
|
+
// Pruning replaces content permanently; retaining a cold-spill marker would
|
|
6160
|
+
// rehydrate the superseded payload on the next materialization.
|
|
6161
|
+
canonical.evictedContent = undefined;
|
|
6162
|
+
}
|
|
6163
|
+
this.#needsFullRewriteOnNextPersist = true;
|
|
6164
|
+
this.#bumpEntryRevision();
|
|
6165
|
+
this.#replayMetadataRevision++;
|
|
6166
|
+
}
|
|
6167
|
+
|
|
4586
6168
|
/**
|
|
4587
6169
|
* Rewrite the session file after in-place entry updates.
|
|
4588
6170
|
* Use sparingly (e.g., pruning old tool outputs).
|
|
@@ -4638,18 +6220,29 @@ export class SessionManager {
|
|
|
4638
6220
|
// TTSR (Time Traveling Stream Rules)
|
|
4639
6221
|
// =========================================================================
|
|
4640
6222
|
|
|
4641
|
-
/**
|
|
4642
|
-
|
|
4643
|
-
* @param selectedToolNames MCP tool names selected for this branch
|
|
4644
|
-
* @returns Entry id
|
|
4645
|
-
*/
|
|
4646
|
-
appendMCPToolSelection(selectedToolNames: string[]): string {
|
|
6223
|
+
/** Append MCP discovery selection authority without altering discovered built-in authority. */
|
|
6224
|
+
appendMCPToolSelection(selectedToolNames: string[], mutationCorrelationId?: string): string {
|
|
4647
6225
|
const entry: MCPToolSelectionEntry = {
|
|
4648
6226
|
type: "mcp_tool_selection",
|
|
4649
6227
|
id: generateId(this.#byId),
|
|
4650
6228
|
parentId: this.#leafId,
|
|
4651
6229
|
timestamp: new Date().toISOString(),
|
|
4652
6230
|
selectedToolNames: [...selectedToolNames],
|
|
6231
|
+
mutationCorrelationId,
|
|
6232
|
+
};
|
|
6233
|
+
this.#appendEntry(entry);
|
|
6234
|
+
return entry.id;
|
|
6235
|
+
}
|
|
6236
|
+
|
|
6237
|
+
/** Append discovered built-in selection authority without altering MCP authority. */
|
|
6238
|
+
appendDiscoveredBuiltinToolSelection(selectedToolNames: string[], mutationCorrelationId?: string): string {
|
|
6239
|
+
const entry: DiscoveredBuiltinToolSelectionEntry = {
|
|
6240
|
+
type: "discovered_builtin_tool_selection",
|
|
6241
|
+
id: generateId(this.#byId),
|
|
6242
|
+
parentId: this.#leafId,
|
|
6243
|
+
timestamp: new Date().toISOString(),
|
|
6244
|
+
selectedToolNames: [...selectedToolNames],
|
|
6245
|
+
mutationCorrelationId,
|
|
4653
6246
|
};
|
|
4654
6247
|
this.#appendEntry(entry);
|
|
4655
6248
|
return entry.id;
|
|
@@ -4702,7 +6295,9 @@ export class SessionManager {
|
|
|
4702
6295
|
getLeafEntry(): SessionEntry | undefined {
|
|
4703
6296
|
if (!this.#leafId) return undefined;
|
|
4704
6297
|
const entry = this.#byId.get(this.#leafId);
|
|
4705
|
-
return entry
|
|
6298
|
+
return entry
|
|
6299
|
+
? cloneSessionEntry(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()))
|
|
6300
|
+
: undefined;
|
|
4706
6301
|
}
|
|
4707
6302
|
|
|
4708
6303
|
getResidentImageBytes(): number {
|
|
@@ -4887,10 +6482,12 @@ export class SessionManager {
|
|
|
4887
6482
|
getEntryForFidelity(id: string): SessionEntry | undefined {
|
|
4888
6483
|
const entry = this.#byId.get(id);
|
|
4889
6484
|
return entry
|
|
4890
|
-
?
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
6485
|
+
? cloneSessionEntry(
|
|
6486
|
+
rehydrateColdSpillEntry(
|
|
6487
|
+
materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()),
|
|
6488
|
+
this.#blobStore,
|
|
6489
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
6490
|
+
),
|
|
4894
6491
|
)
|
|
4895
6492
|
: undefined;
|
|
4896
6493
|
}
|
|
@@ -4904,10 +6501,12 @@ export class SessionManager {
|
|
|
4904
6501
|
if (visited.has(current.id)) break;
|
|
4905
6502
|
visited.add(current.id);
|
|
4906
6503
|
path.push(
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
6504
|
+
cloneSessionEntry(
|
|
6505
|
+
rehydrateColdSpillEntry(
|
|
6506
|
+
materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache),
|
|
6507
|
+
this.#blobStore,
|
|
6508
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
6509
|
+
),
|
|
4911
6510
|
),
|
|
4912
6511
|
);
|
|
4913
6512
|
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
@@ -4944,10 +6543,12 @@ export class SessionManager {
|
|
|
4944
6543
|
return this.#fileEntries
|
|
4945
6544
|
.filter((entry): entry is SessionEntry => entry.type !== "session")
|
|
4946
6545
|
.map(entry =>
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
6546
|
+
cloneSessionEntry(
|
|
6547
|
+
rehydrateColdSpillEntry(
|
|
6548
|
+
materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache),
|
|
6549
|
+
this.#blobStore,
|
|
6550
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
6551
|
+
),
|
|
4951
6552
|
),
|
|
4952
6553
|
);
|
|
4953
6554
|
}
|
|
@@ -4956,7 +6557,9 @@ export class SessionManager {
|
|
|
4956
6557
|
this.#publicMaterializerCallCount++;
|
|
4957
6558
|
this.#getEntryMaterializerCallCount++;
|
|
4958
6559
|
const entry = this.#byId.get(id);
|
|
4959
|
-
return entry
|
|
6560
|
+
return entry
|
|
6561
|
+
? cloneSessionEntry(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()))
|
|
6562
|
+
: undefined;
|
|
4960
6563
|
}
|
|
4961
6564
|
|
|
4962
6565
|
/**
|
|
@@ -4967,7 +6570,9 @@ export class SessionManager {
|
|
|
4967
6570
|
const children: SessionEntry[] = [];
|
|
4968
6571
|
for (const entry of this.#byId.values()) {
|
|
4969
6572
|
if (entry.parentId === parentId) {
|
|
4970
|
-
children.push(
|
|
6573
|
+
children.push(
|
|
6574
|
+
cloneSessionEntry(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache)),
|
|
6575
|
+
);
|
|
4971
6576
|
}
|
|
4972
6577
|
}
|
|
4973
6578
|
return children;
|
|
@@ -5022,7 +6627,7 @@ export class SessionManager {
|
|
|
5022
6627
|
while (current) {
|
|
5023
6628
|
if (visited.has(current.id)) break;
|
|
5024
6629
|
visited.add(current.id);
|
|
5025
|
-
path.push(materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache));
|
|
6630
|
+
path.push(cloneSessionEntry(materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache)));
|
|
5026
6631
|
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
5027
6632
|
}
|
|
5028
6633
|
path.reverse();
|
|
@@ -5033,15 +6638,26 @@ export class SessionManager {
|
|
|
5033
6638
|
* Build the session context (what gets sent to the LLM).
|
|
5034
6639
|
* Uses tree traversal from current leaf.
|
|
5035
6640
|
*/
|
|
6641
|
+
/**
|
|
6642
|
+
* Return a defensive context snapshot for public consumers.
|
|
6643
|
+
*/
|
|
5036
6644
|
buildSessionContext(): SessionContext {
|
|
5037
|
-
|
|
6645
|
+
return cloneSessionContext(this.#getSessionContextForRead());
|
|
6646
|
+
}
|
|
6647
|
+
|
|
6648
|
+
/**
|
|
6649
|
+
* Return the revision-keyed context cache for internal read-only consumers.
|
|
6650
|
+
* The strong reference avoids GC-driven rebuilds during a session.
|
|
6651
|
+
*/
|
|
6652
|
+
#getSessionContextForRead(): Readonly<SessionContext> {
|
|
6653
|
+
const cached = this.#sessionContextCache;
|
|
5038
6654
|
if (
|
|
5039
6655
|
cached &&
|
|
5040
6656
|
this.#sessionContextEntryRevision === this.#entryRevision &&
|
|
5041
6657
|
this.#sessionContextLeafRevision === this.#leafRevision &&
|
|
5042
6658
|
this.#sessionContextReplayMetadataRevision === this.#replayMetadataRevision
|
|
5043
6659
|
) {
|
|
5044
|
-
return
|
|
6660
|
+
return cached;
|
|
5045
6661
|
}
|
|
5046
6662
|
this.#pathOnlyContextBuildCount++;
|
|
5047
6663
|
const context = buildSessionContext(
|
|
@@ -5050,11 +6666,12 @@ export class SessionManager {
|
|
|
5050
6666
|
undefined,
|
|
5051
6667
|
this.#sessionId,
|
|
5052
6668
|
);
|
|
5053
|
-
this.#sessionContextCache =
|
|
6669
|
+
this.#sessionContextCache = freezeInternalReadSnapshot(context);
|
|
6670
|
+
transferSessionMessageIdentity(context.messages, this.#sessionContextCache.messages);
|
|
5054
6671
|
this.#sessionContextEntryRevision = this.#entryRevision;
|
|
5055
6672
|
this.#sessionContextLeafRevision = this.#leafRevision;
|
|
5056
6673
|
this.#sessionContextReplayMetadataRevision = this.#replayMetadataRevision;
|
|
5057
|
-
return
|
|
6674
|
+
return this.#sessionContextCache;
|
|
5058
6675
|
}
|
|
5059
6676
|
|
|
5060
6677
|
#getActivePathEntriesForProviderContext(fromId?: string | null): SessionEntry[] {
|
|
@@ -5109,28 +6726,33 @@ export class SessionManager {
|
|
|
5109
6726
|
const marker = entry.type === "message" || entry.type === "custom_message" ? entry.evictedContent : undefined;
|
|
5110
6727
|
return marker ? Object.keys(marker.payloads ?? {}).length : 0;
|
|
5111
6728
|
}
|
|
5112
|
-
/** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */
|
|
6729
|
+
/** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries without persisting it. */
|
|
5113
6730
|
sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
if (entry.type !== "message" || entry.message.role !== "assistant") {
|
|
5117
|
-
continue;
|
|
5118
|
-
}
|
|
6731
|
+
return this.#sanitizeLoadedOpenAIResponsesReplayMetadata().length > 0;
|
|
6732
|
+
}
|
|
5119
6733
|
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
6734
|
+
async #sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist(): Promise<boolean> {
|
|
6735
|
+
const patches = this.#sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
6736
|
+
for (const patch of patches) {
|
|
6737
|
+
await this.#persistPatch(patch);
|
|
6738
|
+
}
|
|
6739
|
+
return patches.length > 0;
|
|
6740
|
+
}
|
|
5124
6741
|
|
|
6742
|
+
#sanitizeLoadedOpenAIResponsesReplayMetadata(): EntryPatchRecord[] {
|
|
6743
|
+
const patches: EntryPatchRecord[] = [];
|
|
6744
|
+
for (const entry of this.#fileEntries) {
|
|
6745
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
6746
|
+
const sanitizedMessage = sanitizeRehydratedOpenAIResponsesAssistantMessage(entry.message);
|
|
6747
|
+
if (sanitizedMessage === entry.message) continue;
|
|
5125
6748
|
entry.message = sanitizedMessage;
|
|
5126
|
-
|
|
6749
|
+
patches.push({ type: "entry_patch", entryId: entry.id, patch: { message: sanitizedMessage } });
|
|
5127
6750
|
}
|
|
5128
|
-
if (
|
|
6751
|
+
if (patches.length > 0) {
|
|
5129
6752
|
this.#bumpEntryRevision();
|
|
5130
6753
|
this.#replayMetadataRevision++;
|
|
5131
6754
|
}
|
|
5132
|
-
|
|
5133
|
-
return didSanitize;
|
|
6755
|
+
return patches;
|
|
5134
6756
|
}
|
|
5135
6757
|
|
|
5136
6758
|
/**
|
|
@@ -5142,9 +6764,7 @@ export class SessionManager {
|
|
|
5142
6764
|
}
|
|
5143
6765
|
|
|
5144
6766
|
/**
|
|
5145
|
-
*
|
|
5146
|
-
* The session is append-only: use appendXXX() to add entries, branch() to
|
|
5147
|
-
* change the leaf pointer. Entries cannot be modified or deleted.
|
|
6767
|
+
* Internal materialized entry cache. Public getters clone this snapshot before returning it.
|
|
5148
6768
|
*/
|
|
5149
6769
|
#getMaterializedEntriesInternal(): SessionEntry[] {
|
|
5150
6770
|
if (this.#materializedEntriesRevision === this.#entryRevision && this.#materializedEntriesCache) {
|
|
@@ -5170,12 +6790,14 @@ export class SessionManager {
|
|
|
5170
6790
|
}
|
|
5171
6791
|
|
|
5172
6792
|
/**
|
|
5173
|
-
* Get the session as a tree structure. Returns
|
|
6793
|
+
* Get the session as a tree structure. Returns defensive copies of all entries.
|
|
5174
6794
|
* A well-formed session has exactly one root (first entry with parentId === null).
|
|
5175
6795
|
* Orphaned entries (broken parent chain) are also returned as roots.
|
|
5176
6796
|
*/
|
|
5177
6797
|
getTree(): SessionTreeNode[] {
|
|
5178
|
-
|
|
6798
|
+
return this.#getTree(this.getEntries());
|
|
6799
|
+
}
|
|
6800
|
+
#getTree(entries: readonly SessionEntry[]): SessionTreeNode[] {
|
|
5179
6801
|
const nodeMap = new Map<string, SessionTreeNode>();
|
|
5180
6802
|
const roots: SessionTreeNode[] = [];
|
|
5181
6803
|
|
|
@@ -5247,7 +6869,9 @@ export class SessionManager {
|
|
|
5247
6869
|
|
|
5248
6870
|
return roots;
|
|
5249
6871
|
}
|
|
5250
|
-
|
|
6872
|
+
[sessionManagerReadCapability](): SessionManagerReadAccess {
|
|
6873
|
+
return this.#internalReadAccess;
|
|
6874
|
+
}
|
|
5251
6875
|
// =========================================================================
|
|
5252
6876
|
// Branching
|
|
5253
6877
|
// =========================================================================
|
|
@@ -5365,7 +6989,14 @@ export class SessionManager {
|
|
|
5365
6989
|
labelEntries.push(labelEntry);
|
|
5366
6990
|
parentId = labelEntry.id;
|
|
5367
6991
|
}
|
|
5368
|
-
this.
|
|
6992
|
+
if (this.destination.kind === "managed") {
|
|
6993
|
+
this.#managedTranscriptStore(newSessionFile).publishNoReplaceSync(
|
|
6994
|
+
path.basename(newSessionFile),
|
|
6995
|
+
Buffer.from(`${lines.join("\n")}\n`, "utf8"),
|
|
6996
|
+
);
|
|
6997
|
+
} else {
|
|
6998
|
+
this.storage.writeTextSync(newSessionFile, `${lines.join("\n")}\n`);
|
|
6999
|
+
}
|
|
5369
7000
|
this.#sessionId = newSessionId;
|
|
5370
7001
|
this.#sessionFile = newSessionFile;
|
|
5371
7002
|
this.#resetResidentTextBlobStore();
|
|
@@ -5422,18 +7053,121 @@ export class SessionManager {
|
|
|
5422
7053
|
return computeDefaultSessionDir(cwd, storage, getSessionsDir(agentDir));
|
|
5423
7054
|
}
|
|
5424
7055
|
|
|
7056
|
+
/** Resolve the default session directory without creating or migrating storage. */
|
|
7057
|
+
static getDefaultSessionDirReadOnly(cwd: string, agentDir?: string): string {
|
|
7058
|
+
const sessionsRoot = getSessionsDir(agentDir);
|
|
7059
|
+
const resolved = resolveManagedScope({
|
|
7060
|
+
cwd,
|
|
7061
|
+
agentDir: agentDir ?? path.resolve(sessionsRoot, ".."),
|
|
7062
|
+
sessionsRoot,
|
|
7063
|
+
});
|
|
7064
|
+
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
7065
|
+
return resolved.scope.directoryPath;
|
|
7066
|
+
}
|
|
7067
|
+
|
|
5425
7068
|
/**
|
|
5426
7069
|
* Create a new session.
|
|
5427
7070
|
* @param cwd Working directory (stored in session header)
|
|
5428
7071
|
* @param sessionDir Optional session directory. If omitted, uses default (~/.skc/agent/sessions/<encoded-cwd>/).
|
|
5429
7072
|
*/
|
|
5430
|
-
static
|
|
5431
|
-
|
|
5432
|
-
|
|
7073
|
+
static nestedManagedDestination(
|
|
7074
|
+
authority: ManagedDirectoryRoot | ManagedSessionDescendantStore,
|
|
7075
|
+
directory: string,
|
|
7076
|
+
): SessionDestination {
|
|
7077
|
+
const rootAuthority =
|
|
7078
|
+
authority instanceof ManagedSessionDescendantStore ? authority.subtreeRootAuthority : authority;
|
|
7079
|
+
const retainedAuthority =
|
|
7080
|
+
authority instanceof ManagedSessionDescendantStore
|
|
7081
|
+
? authority.retainAuthority()
|
|
7082
|
+
: retainManagedDirectoryAuthority(rootAuthority, directory);
|
|
7083
|
+
const securityContext = createManagedSessionSecurityContext({
|
|
7084
|
+
agentDir: rootAuthority.canonicalPath,
|
|
7085
|
+
sessionsRoot: rootAuthority.canonicalPath,
|
|
7086
|
+
sessionDir: directory,
|
|
7087
|
+
rootAuthority,
|
|
7088
|
+
retainedAuthority,
|
|
7089
|
+
});
|
|
7090
|
+
const destination = Object.freeze({ kind: "managed" as const, directory, securityContext });
|
|
7091
|
+
trustedSessionDestinations.add(destination);
|
|
7092
|
+
return destination;
|
|
7093
|
+
}
|
|
7094
|
+
|
|
7095
|
+
static managedDestination(
|
|
7096
|
+
cwd: string,
|
|
7097
|
+
agentDir?: string,
|
|
7098
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7099
|
+
): SessionDestination {
|
|
7100
|
+
return managedDestination(cwd, storage, agentDir);
|
|
7101
|
+
}
|
|
7102
|
+
|
|
7103
|
+
static explicitDestination(directory: string): SessionDestination {
|
|
7104
|
+
return explicitDestination(directory);
|
|
7105
|
+
}
|
|
7106
|
+
|
|
7107
|
+
static create(
|
|
7108
|
+
cwd: string,
|
|
7109
|
+
destinationInput?: SessionDestinationInput,
|
|
7110
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7111
|
+
): SessionManager {
|
|
7112
|
+
const destination = destinationFor(cwd, destinationInput, storage);
|
|
7113
|
+
const manager = new SessionManager(cwd, destination.directory, true, storage, destination);
|
|
5433
7114
|
manager.#initNewSession();
|
|
5434
7115
|
return manager;
|
|
5435
7116
|
}
|
|
5436
7117
|
|
|
7118
|
+
/** Resolve a default-managed candidate through binding validation and copy-retain migration before mutation. */
|
|
7119
|
+
static async prepareManagedCandidateForWrite(
|
|
7120
|
+
filePath: string,
|
|
7121
|
+
migrationPolicy: SessionDirectoryMigrationPolicy,
|
|
7122
|
+
): Promise<string> {
|
|
7123
|
+
const storage = new FileSessionStorage();
|
|
7124
|
+
const inspected = inspectResumeSessionFile(filePath, storage);
|
|
7125
|
+
if ("kind" in inspected) {
|
|
7126
|
+
if (inspected.reason === "missing") return filePath;
|
|
7127
|
+
throw new Error(`Could not inspect managed session: ${inspected.reason}`);
|
|
7128
|
+
}
|
|
7129
|
+
const header = inspected.entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
7130
|
+
if (!header?.cwd) return filePath;
|
|
7131
|
+
const sessionsRoot = getSessionsDir();
|
|
7132
|
+
const resolvedPath = path.resolve(filePath);
|
|
7133
|
+
const relativeToManagedRoot = path.relative(sessionsRoot, resolvedPath);
|
|
7134
|
+
const isManagedPath =
|
|
7135
|
+
relativeToManagedRoot === "" ||
|
|
7136
|
+
(!relativeToManagedRoot.startsWith("..") && !path.isAbsolute(relativeToManagedRoot));
|
|
7137
|
+
|
|
7138
|
+
if (!isManagedPath) return filePath;
|
|
7139
|
+
const resolved = resolveManagedScope({
|
|
7140
|
+
cwd: header.cwd,
|
|
7141
|
+
agentDir: path.resolve(sessionsRoot, ".."),
|
|
7142
|
+
sessionsRoot,
|
|
7143
|
+
});
|
|
7144
|
+
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
7145
|
+
|
|
7146
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
7147
|
+
if (listing.kind !== "complete") throw new Error(`Managed session scan failed: ${listing.message}`);
|
|
7148
|
+
|
|
7149
|
+
const candidate = listing.owned.find(item => path.resolve(item.path) === resolvedPath);
|
|
7150
|
+
if (!candidate) throw new Error("Session is inside managed storage but is not an authorized managed candidate.");
|
|
7151
|
+
const revalidated = inspectResumeSessionFile(filePath, storage);
|
|
7152
|
+
if (
|
|
7153
|
+
"kind" in revalidated ||
|
|
7154
|
+
!sameResumeIdentity(inspected.identity, revalidated.identity) ||
|
|
7155
|
+
candidate.identity.dev !== inspected.identity.dev ||
|
|
7156
|
+
candidate.identity.ino !== inspected.identity.ino ||
|
|
7157
|
+
candidate.identity.size !== inspected.identity.size ||
|
|
7158
|
+
candidate.identity.mtimeNs !== inspected.identity.mtimeNs ||
|
|
7159
|
+
candidate.identity.mtimeMs !== inspected.identity.mtimeMs ||
|
|
7160
|
+
candidate.identity.sha256 !== inspected.identity.sha256
|
|
7161
|
+
)
|
|
7162
|
+
throw new Error("Managed session changed before migration authority was adopted.");
|
|
7163
|
+
const opened = await openManagedCandidateForWrite(resolved.scope, candidate, migrationPolicy);
|
|
7164
|
+
if (opened.kind === "error") {
|
|
7165
|
+
if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
|
|
7166
|
+
throw new Error(`Could not open managed session: ${opened.message}`);
|
|
7167
|
+
}
|
|
7168
|
+
return opened.path;
|
|
7169
|
+
}
|
|
7170
|
+
|
|
5437
7171
|
/**
|
|
5438
7172
|
* Fork a session into the current project directory.
|
|
5439
7173
|
* Copies history from another session file while creating a new session file in the current sessionDir.
|
|
@@ -5441,12 +7175,18 @@ export class SessionManager {
|
|
|
5441
7175
|
static async forkFrom(
|
|
5442
7176
|
sourcePath: string,
|
|
5443
7177
|
cwd: string,
|
|
5444
|
-
|
|
7178
|
+
destinationInput?: SessionDestinationInput,
|
|
5445
7179
|
storage: SessionStorage = new FileSessionStorage(),
|
|
7180
|
+
migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
|
|
5446
7181
|
): Promise<SessionManager> {
|
|
5447
|
-
const
|
|
5448
|
-
const
|
|
5449
|
-
|
|
7182
|
+
const destination = destinationFor(cwd, destinationInput, storage);
|
|
7183
|
+
const managedSourcePath =
|
|
7184
|
+
destination.kind === "managed" && storage instanceof FileSessionStorage
|
|
7185
|
+
? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy)
|
|
7186
|
+
: sourcePath;
|
|
7187
|
+
const dir = destination.directory;
|
|
7188
|
+
const manager = new SessionManager(cwd, dir, true, storage, destination);
|
|
7189
|
+
const forkEntries = structuredClone(await loadEntriesFromFile(managedSourcePath, storage)) as FileEntry[];
|
|
5450
7190
|
migrateToCurrentVersion(forkEntries);
|
|
5451
7191
|
await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
|
|
5452
7192
|
manager.#fileEntries = forkEntries;
|
|
@@ -5479,20 +7219,103 @@ export class SessionManager {
|
|
|
5479
7219
|
*/
|
|
5480
7220
|
static async open(
|
|
5481
7221
|
filePath: string,
|
|
5482
|
-
|
|
7222
|
+
destinationInput?: SessionDestinationInput,
|
|
5483
7223
|
storage: SessionStorage = new FileSessionStorage(),
|
|
7224
|
+
migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
|
|
5484
7225
|
): Promise<SessionManager> {
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
//
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
7226
|
+
const destination =
|
|
7227
|
+
destinationInput === undefined
|
|
7228
|
+
? explicitDestination(path.dirname(filePath))
|
|
7229
|
+
: destinationFor(getProjectDir(), destinationInput, storage);
|
|
7230
|
+
// Canonicalize benign ancestor symlinks in the caller-supplied path (e.g.
|
|
7231
|
+
// macOS `/var -> /private/var`, a symlinked `$HOME`/project) so explicit
|
|
7232
|
+
// session files land under a symlink-free trusted root that the strict
|
|
7233
|
+
// owner-only and reparse guards accept.
|
|
7234
|
+
if (storage instanceof FileSessionStorage) filePath = canonicalizeTrustedPath(filePath);
|
|
7235
|
+
if (destination.kind === "explicit" || !(storage instanceof FileSessionStorage)) {
|
|
7236
|
+
const entries = await loadEntriesFromFile(filePath, storage);
|
|
7237
|
+
const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
7238
|
+
const manager = new SessionManager(
|
|
7239
|
+
header?.cwd ?? getProjectDir(),
|
|
7240
|
+
destination.directory,
|
|
7241
|
+
true,
|
|
7242
|
+
storage,
|
|
7243
|
+
destination,
|
|
7244
|
+
);
|
|
7245
|
+
await manager.#initSessionFile(filePath);
|
|
7246
|
+
return manager;
|
|
7247
|
+
}
|
|
7248
|
+
|
|
7249
|
+
const managedPath = await SessionManager.prepareManagedCandidateForWrite(filePath, migrationPolicy);
|
|
7250
|
+
const inspected = inspectResumeSessionFile(managedPath, storage);
|
|
7251
|
+
if ("kind" in inspected) {
|
|
7252
|
+
if (inspected.reason === "missing" && path.resolve(managedPath) === path.resolve(filePath)) {
|
|
7253
|
+
const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
|
|
7254
|
+
await manager.#initSessionFile(filePath);
|
|
7255
|
+
return manager;
|
|
7256
|
+
}
|
|
7257
|
+
throw new Error(`Could not open session: ${inspected.reason}`);
|
|
7258
|
+
}
|
|
7259
|
+
const opened = await SessionManager.openExistingStrict(inspected.identity, destination, storage, migrationPolicy);
|
|
7260
|
+
if (opened.kind === "error") throw new Error(`Could not open session: ${opened.reason}`);
|
|
7261
|
+
return opened.manager;
|
|
5494
7262
|
}
|
|
5495
7263
|
|
|
7264
|
+
static async openNestedManaged(
|
|
7265
|
+
filePath: string,
|
|
7266
|
+
destination: SessionDestination,
|
|
7267
|
+
store: ManagedSessionDescendantStore,
|
|
7268
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7269
|
+
): Promise<SessionManager> {
|
|
7270
|
+
if (destination.kind !== "managed" || !trustedSessionDestinations.has(destination))
|
|
7271
|
+
throw new Error("Nested managed session authority is unavailable");
|
|
7272
|
+
const resolved = path.resolve(filePath);
|
|
7273
|
+
if (
|
|
7274
|
+
path.dirname(resolved) !== path.resolve(store.dir) ||
|
|
7275
|
+
path.dirname(resolved) !== path.resolve(destination.directory)
|
|
7276
|
+
)
|
|
7277
|
+
throw new Error("Nested managed session escaped retained authority");
|
|
7278
|
+
store.assertBound();
|
|
7279
|
+
const captured = store.readExpected(path.basename(resolved));
|
|
7280
|
+
const entries = captured ? parseSessionEntries(captured.bytes.toString("utf8")) : [];
|
|
7281
|
+
const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
7282
|
+
const manager = new SessionManager(
|
|
7283
|
+
header?.cwd ?? getProjectDir(),
|
|
7284
|
+
destination.directory,
|
|
7285
|
+
true,
|
|
7286
|
+
storage,
|
|
7287
|
+
destination,
|
|
7288
|
+
);
|
|
7289
|
+
manager.#sessionFile = resolved;
|
|
7290
|
+
writeTerminalBreadcrumb(manager.cwd, resolved);
|
|
7291
|
+
manager.#fileEntries = entries;
|
|
7292
|
+
if (entries.length > 0) {
|
|
7293
|
+
manager.#sessionId = header?.id ?? createSessionId();
|
|
7294
|
+
manager.#sessionName = header?.title;
|
|
7295
|
+
manager.#titleSource = header?.titleSource;
|
|
7296
|
+
manager.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(manager.#fileEntries);
|
|
7297
|
+
await resolveBlobRefsInEntries(manager.#fileEntries, manager.#blobStore);
|
|
7298
|
+
manager.#resetResidentTextBlobStore();
|
|
7299
|
+
manager.#fileEntries = manager.#fileEntries.map(entry =>
|
|
7300
|
+
prepareEntryForResidentSync(entry, manager.#residentBlobStores()),
|
|
7301
|
+
);
|
|
7302
|
+
manager.#buildIndex();
|
|
7303
|
+
manager.#bumpAllRevisions();
|
|
7304
|
+
manager.#flushed = true;
|
|
7305
|
+
manager.#ensuredOnDisk = true;
|
|
7306
|
+
await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
|
|
7307
|
+
} else {
|
|
7308
|
+
manager.#newSessionSync();
|
|
7309
|
+
manager.#sessionFile = resolved;
|
|
7310
|
+
manager.#resetResidentTextBlobStore();
|
|
7311
|
+
await manager.#rewriteFile();
|
|
7312
|
+
manager.#flushed = true;
|
|
7313
|
+
manager.#ensuredOnDisk = true;
|
|
7314
|
+
manager.#bumpAllRevisions();
|
|
7315
|
+
}
|
|
7316
|
+
store.assertBound();
|
|
7317
|
+
return manager;
|
|
7318
|
+
}
|
|
5496
7319
|
/**
|
|
5497
7320
|
* List sessions for the resume picker without recovery or other maintenance writes.
|
|
5498
7321
|
*/
|
|
@@ -5501,16 +7324,247 @@ export class SessionManager {
|
|
|
5501
7324
|
sessionDir?: string,
|
|
5502
7325
|
storage: SessionStorage = new FileSessionStorage(),
|
|
5503
7326
|
): Promise<SessionInfo[]> {
|
|
5504
|
-
|
|
7327
|
+
if (!sessionDir && !(storage instanceof FileSessionStorage)) return [];
|
|
7328
|
+
if (sessionDir && !resolveManagedSessionRoot(sessionDir, cwd)) {
|
|
7329
|
+
try {
|
|
7330
|
+
return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
|
|
7331
|
+
} catch {
|
|
7332
|
+
return [];
|
|
7333
|
+
}
|
|
7334
|
+
}
|
|
7335
|
+
const sessionsRoot = sessionDir
|
|
7336
|
+
? (resolveManagedSessionRoot(sessionDir, cwd) ?? getSessionsDir())
|
|
7337
|
+
: getSessionsDir();
|
|
7338
|
+
const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
|
|
7339
|
+
if (resolved.kind === "error") return [];
|
|
7340
|
+
if (storage instanceof FileSessionStorage) {
|
|
7341
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
7342
|
+
if (listing.kind === "error") return [];
|
|
7343
|
+
return await collectSessionsFromFiles(
|
|
7344
|
+
listing.owned.map(candidate => candidate.path),
|
|
7345
|
+
storage,
|
|
7346
|
+
);
|
|
7347
|
+
}
|
|
7348
|
+
const legacyDirectory = `--${resolved.scope.canonicalCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
7349
|
+
const files = new Set<string>();
|
|
5505
7350
|
try {
|
|
5506
|
-
const
|
|
5507
|
-
|
|
5508
|
-
for (const filePath of storage.listFilesSync(dir, "*.jsonl")) files.add(filePath);
|
|
7351
|
+
for (const directory of [resolved.scope.directoryPath, path.join(sessionsRoot, legacyDirectory)]) {
|
|
7352
|
+
for (const file of storage.listFilesSync(directory, "*.jsonl")) files.add(file);
|
|
5509
7353
|
}
|
|
5510
|
-
return await collectSessionsFromFiles([...files], storage);
|
|
5511
7354
|
} catch {
|
|
5512
7355
|
return [];
|
|
5513
7356
|
}
|
|
7357
|
+
return (await collectSessionsFromFiles([...files], storage)).filter(
|
|
7358
|
+
session => resolveEquivalentPath(session.cwd) === resolved.scope.canonicalCwd,
|
|
7359
|
+
);
|
|
7360
|
+
}
|
|
7361
|
+
|
|
7362
|
+
/** Tombstone and exact-delete a default-managed candidate. */
|
|
7363
|
+
static async deleteManagedCandidate(sessionPath: string): Promise<void> {
|
|
7364
|
+
const storage = new FileSessionStorage();
|
|
7365
|
+
const entries = await loadEntriesFromFile(sessionPath, storage);
|
|
7366
|
+
const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
7367
|
+
if (!header?.cwd) throw new Error("Session has no valid managed workspace header.");
|
|
7368
|
+
const sessionsRoot = path.resolve(sessionPath, "../..");
|
|
7369
|
+
const resolved = resolveManagedScope({
|
|
7370
|
+
cwd: header.cwd,
|
|
7371
|
+
agentDir: path.resolve(sessionsRoot, ".."),
|
|
7372
|
+
sessionsRoot,
|
|
7373
|
+
});
|
|
7374
|
+
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
7375
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
7376
|
+
if (listing.kind !== "complete") throw new Error("Managed session scan did not grant deletion authority.");
|
|
7377
|
+
const candidate = listing.owned.find(item => path.resolve(item.path) === path.resolve(sessionPath));
|
|
7378
|
+
if (!candidate) throw new Error("Session is not an authorized managed candidate.");
|
|
7379
|
+
const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
|
|
7380
|
+
if (deleted.kind === "error") throw new Error(`Could not delete managed session: ${deleted.message}`);
|
|
7381
|
+
}
|
|
7382
|
+
|
|
7383
|
+
/** Capture exact source content for a strict fork without granting write ownership. */
|
|
7384
|
+
static captureTranscriptStrict(
|
|
7385
|
+
filePath: string,
|
|
7386
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7387
|
+
): StrictSessionCaptureResult {
|
|
7388
|
+
const inspected = inspectResumeSessionFile(filePath, storage);
|
|
7389
|
+
if ("kind" in inspected) return inspected;
|
|
7390
|
+
return {
|
|
7391
|
+
kind: "captured",
|
|
7392
|
+
snapshot: {
|
|
7393
|
+
sourcePath: path.resolve(filePath),
|
|
7394
|
+
identity: inspected.identity,
|
|
7395
|
+
content: Uint8Array.from(inspected.content),
|
|
7396
|
+
storage,
|
|
7397
|
+
},
|
|
7398
|
+
};
|
|
7399
|
+
}
|
|
7400
|
+
|
|
7401
|
+
/**
|
|
7402
|
+
* Fork strictly from captured source bytes. The source pathname is used only to
|
|
7403
|
+
* revalidate captured authority before destination initialization and transcript
|
|
7404
|
+
* persistence; destination history always comes from the captured bytes.
|
|
7405
|
+
*/
|
|
7406
|
+
static async forkFromCaptured(
|
|
7407
|
+
snapshot: CapturedSessionTranscriptSnapshot,
|
|
7408
|
+
cwd: string,
|
|
7409
|
+
destinationInput?: SessionDestinationInput,
|
|
7410
|
+
_migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
|
|
7411
|
+
): Promise<StrictSessionForkResult> {
|
|
7412
|
+
const destination = destinationFor(cwd, destinationInput, snapshot.storage);
|
|
7413
|
+
if (crypto.createHash("sha256").update(snapshot.content).digest("hex") !== snapshot.identity.sha256) {
|
|
7414
|
+
return { kind: "error", reason: "identity-mismatch" };
|
|
7415
|
+
}
|
|
7416
|
+
|
|
7417
|
+
let forkEntries: FileEntry[] = [];
|
|
7418
|
+
try {
|
|
7419
|
+
const content = new TextDecoder("utf-8", { fatal: true }).decode(snapshot.content);
|
|
7420
|
+
for (const line of content.split(/\r?\n/)) {
|
|
7421
|
+
if (line.length > 0) JSON.parse(line);
|
|
7422
|
+
}
|
|
7423
|
+
forkEntries = parseSessionEntries(content);
|
|
7424
|
+
const sourceHeader = forkEntries[0] as SessionHeader | undefined;
|
|
7425
|
+
if (sourceHeader?.type !== "session" || sourceHeader.id !== snapshot.identity.sessionId)
|
|
7426
|
+
return { kind: "error", reason: "identity-mismatch" };
|
|
7427
|
+
migrateToCurrentVersion(forkEntries);
|
|
7428
|
+
if (!hasStrictSessionSchema(forkEntries)) return { kind: "error", reason: "malformed" };
|
|
7429
|
+
} catch {
|
|
7430
|
+
return { kind: "error", reason: "malformed" };
|
|
7431
|
+
}
|
|
7432
|
+
|
|
7433
|
+
const revalidated = inspectResumeSessionFile(snapshot.sourcePath, snapshot.storage);
|
|
7434
|
+
if ("kind" in revalidated) return revalidated;
|
|
7435
|
+
if (!sameResumeIdentity(snapshot.identity, revalidated.identity)) {
|
|
7436
|
+
return { kind: "error", reason: "identity-mismatch" };
|
|
7437
|
+
}
|
|
7438
|
+
|
|
7439
|
+
const dir = destination.directory;
|
|
7440
|
+
const removeSessionDirOnFailure =
|
|
7441
|
+
destination.kind !== "managed" &&
|
|
7442
|
+
snapshot.storage instanceof FileSessionStorage &&
|
|
7443
|
+
!snapshot.storage.existsSync(dir);
|
|
7444
|
+
let managedForkStore: ManagedSessionDescendantStore | undefined;
|
|
7445
|
+
let managedForkTranscript: ManagedFileSnapshot | null = null;
|
|
7446
|
+
let manager: SessionManager | undefined;
|
|
7447
|
+
let authorityFailure: StrictSessionOpenFailure | undefined;
|
|
7448
|
+
try {
|
|
7449
|
+
manager = new SessionManager(cwd, dir, true, snapshot.storage, destination);
|
|
7450
|
+
await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
|
|
7451
|
+
manager.#fileEntries = forkEntries;
|
|
7452
|
+
const sourceHeader = manager.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
7453
|
+
const historyEntries = manager.#fileEntries.filter(entry => entry.type !== "session") as SessionEntry[];
|
|
7454
|
+
manager.#newSessionSync({ parentSession: sourceHeader?.id }, false);
|
|
7455
|
+
manager.#resetResidentTextBlobStore();
|
|
7456
|
+
const newHeader = manager.#fileEntries[0] as SessionHeader;
|
|
7457
|
+
newHeader.title = sourceHeader?.title;
|
|
7458
|
+
newHeader.titleSource = sourceHeader?.titleSource;
|
|
7459
|
+
const residentBlobStores = manager.#residentBlobStores();
|
|
7460
|
+
manager.#fileEntries = [
|
|
7461
|
+
newHeader,
|
|
7462
|
+
...historyEntries.map(entry => prepareEntryForResidentSync(entry, residentBlobStores) as SessionEntry),
|
|
7463
|
+
];
|
|
7464
|
+
manager.#sessionName = newHeader.title;
|
|
7465
|
+
manager.#titleSource = newHeader.titleSource;
|
|
7466
|
+
manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
7467
|
+
manager.#buildIndex();
|
|
7468
|
+
manager.#bumpAllRevisions();
|
|
7469
|
+
const beforeWrite = inspectResumeSessionFile(snapshot.sourcePath, snapshot.storage);
|
|
7470
|
+
if ("kind" in beforeWrite) {
|
|
7471
|
+
authorityFailure = beforeWrite;
|
|
7472
|
+
throw new Error("Captured fork source authority changed before destination write.");
|
|
7473
|
+
}
|
|
7474
|
+
if (!sameResumeIdentity(snapshot.identity, beforeWrite.identity)) {
|
|
7475
|
+
authorityFailure = { kind: "error", reason: "identity-mismatch" };
|
|
7476
|
+
throw new Error("Captured fork source authority changed before destination write.");
|
|
7477
|
+
}
|
|
7478
|
+
await manager.#rewriteFile();
|
|
7479
|
+
if (destination.kind === "managed" && manager.#sessionFile) {
|
|
7480
|
+
managedForkStore = manager.#managedTranscriptStore();
|
|
7481
|
+
managedForkTranscript = managedForkStore.readExpected(path.basename(manager.#sessionFile));
|
|
7482
|
+
if (!managedForkTranscript) throw new Error("managed_fork_transcript_publish_missing");
|
|
7483
|
+
}
|
|
7484
|
+
const afterWrite = inspectResumeSessionFile(snapshot.sourcePath, snapshot.storage);
|
|
7485
|
+
if ("kind" in afterWrite) {
|
|
7486
|
+
authorityFailure = afterWrite;
|
|
7487
|
+
throw new Error("Captured fork source authority changed during destination write.");
|
|
7488
|
+
}
|
|
7489
|
+
if (!sameResumeIdentity(snapshot.identity, afterWrite.identity)) {
|
|
7490
|
+
authorityFailure = { kind: "error", reason: "identity-mismatch" };
|
|
7491
|
+
throw new Error("Captured fork source authority changed during destination write.");
|
|
7492
|
+
}
|
|
7493
|
+
if (managedForkStore && managedForkTranscript && manager.#sessionFile) {
|
|
7494
|
+
const terminalTranscript = managedForkStore.readExpected(path.basename(manager.#sessionFile));
|
|
7495
|
+
if (!managedFileSnapshotEquals(terminalTranscript, managedForkTranscript))
|
|
7496
|
+
throw new Error("managed_fork_transcript_changed");
|
|
7497
|
+
}
|
|
7498
|
+
if (manager.#sessionFile) writeTerminalBreadcrumb(manager.cwd, manager.#sessionFile);
|
|
7499
|
+
return { kind: "forked", manager };
|
|
7500
|
+
} catch (error) {
|
|
7501
|
+
if (manager) {
|
|
7502
|
+
const sessionFile = manager.#sessionFile;
|
|
7503
|
+
const cleanupErrors: Error[] = [];
|
|
7504
|
+
try {
|
|
7505
|
+
await manager.close();
|
|
7506
|
+
} catch (cleanupError) {
|
|
7507
|
+
cleanupErrors.push(toError(cleanupError));
|
|
7508
|
+
}
|
|
7509
|
+
if (sessionFile) {
|
|
7510
|
+
if (destination.kind === "managed") {
|
|
7511
|
+
try {
|
|
7512
|
+
if (managedForkStore && managedForkTranscript)
|
|
7513
|
+
managedForkStore.removeExpected(path.basename(sessionFile), managedForkTranscript);
|
|
7514
|
+
} catch (cleanupError) {
|
|
7515
|
+
cleanupErrors.push(toError(cleanupError));
|
|
7516
|
+
}
|
|
7517
|
+
} else {
|
|
7518
|
+
try {
|
|
7519
|
+
await manager.storage.unlink(sessionFile);
|
|
7520
|
+
} catch (cleanupError) {
|
|
7521
|
+
if (!isEnoent(cleanupError)) cleanupErrors.push(toError(cleanupError));
|
|
7522
|
+
}
|
|
7523
|
+
if (manager.storage instanceof FileSessionStorage) {
|
|
7524
|
+
try {
|
|
7525
|
+
await fs.promises.rm(sessionFile.slice(0, -6), { recursive: true, force: true });
|
|
7526
|
+
} catch (cleanupError) {
|
|
7527
|
+
cleanupErrors.push(toError(cleanupError));
|
|
7528
|
+
}
|
|
7529
|
+
try {
|
|
7530
|
+
const sessionFileName = path.basename(sessionFile);
|
|
7531
|
+
const transientPrefix = `.${sessionFileName}.`;
|
|
7532
|
+
const backupPrefix = `${sessionFileName}.`;
|
|
7533
|
+
for (const name of await fs.promises.readdir(path.dirname(sessionFile))) {
|
|
7534
|
+
if (
|
|
7535
|
+
(name.startsWith(transientPrefix) && name.endsWith(".tmp")) ||
|
|
7536
|
+
(name.startsWith(backupPrefix) && name.endsWith(".bak"))
|
|
7537
|
+
) {
|
|
7538
|
+
await fs.promises.rm(path.join(path.dirname(sessionFile), name), {
|
|
7539
|
+
recursive: true,
|
|
7540
|
+
force: true,
|
|
7541
|
+
});
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
} catch (cleanupError) {
|
|
7545
|
+
if (!isEnoent(cleanupError)) cleanupErrors.push(toError(cleanupError));
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
if (removeSessionDirOnFailure) {
|
|
7551
|
+
try {
|
|
7552
|
+
await fs.promises.rmdir(dir);
|
|
7553
|
+
} catch (cleanupError) {
|
|
7554
|
+
const code = (cleanupError as NodeJS.ErrnoException).code;
|
|
7555
|
+
if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST")
|
|
7556
|
+
cleanupErrors.push(toError(cleanupError));
|
|
7557
|
+
}
|
|
7558
|
+
}
|
|
7559
|
+
if (cleanupErrors.length > 0) {
|
|
7560
|
+
throw new Error(`Failed to clean up fork destination: ${cleanupErrors[0]!.message}`, {
|
|
7561
|
+
cause: toError(error),
|
|
7562
|
+
});
|
|
7563
|
+
}
|
|
7564
|
+
if (authorityFailure) return authorityFailure;
|
|
7565
|
+
}
|
|
7566
|
+
throw error;
|
|
7567
|
+
}
|
|
5514
7568
|
}
|
|
5515
7569
|
|
|
5516
7570
|
/** Inspect a selected session without acquiring write-capable ownership. */
|
|
@@ -5525,6 +7579,67 @@ export class SessionManager {
|
|
|
5525
7579
|
: { kind: "terminal", identity: inspected.identity };
|
|
5526
7580
|
}
|
|
5527
7581
|
|
|
7582
|
+
/**
|
|
7583
|
+
* Hydrate an existing predecessor transcript for recovery without taking any
|
|
7584
|
+
* write-capable action. The caller must retain the returned context and use
|
|
7585
|
+
* the explicit promotion seam only after its ownership-ready fence and writer
|
|
7586
|
+
* lease are durable.
|
|
7587
|
+
*/
|
|
7588
|
+
static async openExistingForRecoveryHydrationStrict(
|
|
7589
|
+
identity: ResumeSessionIdentity,
|
|
7590
|
+
destinationInput?: SessionDestinationInput,
|
|
7591
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7592
|
+
): Promise<RecoveryHydrationOpenResult> {
|
|
7593
|
+
const destination =
|
|
7594
|
+
destinationInput === undefined
|
|
7595
|
+
? explicitDestination(path.dirname(identity.canonicalPath))
|
|
7596
|
+
: destinationFor(getProjectDir(), destinationInput, storage);
|
|
7597
|
+
const inspected = inspectResumeSessionFile(identity.canonicalPath, storage);
|
|
7598
|
+
if ("kind" in inspected) return inspected;
|
|
7599
|
+
if (!sameResumeIdentity(identity, inspected.identity)) return { kind: "error", reason: "identity-mismatch" };
|
|
7600
|
+
if (inspected.migrationApplied) return { kind: "error", reason: "migration-required" };
|
|
7601
|
+
|
|
7602
|
+
const header = inspected.entries[0] as SessionHeader;
|
|
7603
|
+
const manager = new SessionManager(
|
|
7604
|
+
header.cwd || getProjectDir(),
|
|
7605
|
+
destination.directory,
|
|
7606
|
+
true,
|
|
7607
|
+
storage,
|
|
7608
|
+
destination,
|
|
7609
|
+
);
|
|
7610
|
+
await manager.#hydrateExistingSession(identity.canonicalPath, inspected.entries, false);
|
|
7611
|
+
const revalidated = inspectResumeSessionFile(identity.canonicalPath, storage);
|
|
7612
|
+
if ("kind" in revalidated || !sameResumeIdentity(identity, revalidated.identity)) {
|
|
7613
|
+
await manager.close();
|
|
7614
|
+
return "kind" in revalidated ? revalidated : { kind: "error", reason: "identity-mismatch" };
|
|
7615
|
+
}
|
|
7616
|
+
const context: RecoveryHydrationContext = Object.freeze({
|
|
7617
|
+
identity: Object.freeze({ ...identity }),
|
|
7618
|
+
});
|
|
7619
|
+
manager.#recoveryHydrationContext = context;
|
|
7620
|
+
return { kind: "hydrated", manager, context };
|
|
7621
|
+
}
|
|
7622
|
+
|
|
7623
|
+
/**
|
|
7624
|
+
* Allows the normal post-open metadata sanitation only after the caller has
|
|
7625
|
+
* fsynced its external ownership-ready fence and acquired the writer lease.
|
|
7626
|
+
*/
|
|
7627
|
+
async promoteRecoveryHydrationAfterOwnershipReadyFence(
|
|
7628
|
+
context: RecoveryHydrationContext,
|
|
7629
|
+
fence: RecoveryHydrationPromotionFence,
|
|
7630
|
+
): Promise<void> {
|
|
7631
|
+
if (fence.ownershipReady !== true || this.#recoveryHydrationContext !== context) {
|
|
7632
|
+
throw new Error("Recovery hydration promotion requires the original ownership-ready context.");
|
|
7633
|
+
}
|
|
7634
|
+
const inspected = inspectResumeSessionFile(context.identity.canonicalPath, this.storage);
|
|
7635
|
+
if ("kind" in inspected || !sameResumeIdentity(context.identity, inspected.identity)) {
|
|
7636
|
+
throw new Error("Recovery transcript authority changed before promotion.");
|
|
7637
|
+
}
|
|
7638
|
+
await this.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
|
|
7639
|
+
writeTerminalBreadcrumb(this.cwd, context.identity.canonicalPath);
|
|
7640
|
+
this.#recoveryHydrationContext = undefined;
|
|
7641
|
+
}
|
|
7642
|
+
|
|
5528
7643
|
/**
|
|
5529
7644
|
* Main startup code MUST retain the consented inspection identity and branch on
|
|
5530
7645
|
* the returned discriminant; an error result never creates, rewrites, or adopts
|
|
@@ -5532,29 +7647,54 @@ export class SessionManager {
|
|
|
5532
7647
|
*/
|
|
5533
7648
|
static async openExistingStrict(
|
|
5534
7649
|
identity: ResumeSessionIdentity,
|
|
5535
|
-
|
|
7650
|
+
destinationInput?: SessionDestinationInput,
|
|
5536
7651
|
storage: SessionStorage = new FileSessionStorage(),
|
|
7652
|
+
migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
|
|
5537
7653
|
): Promise<StrictSessionOpenResult> {
|
|
5538
|
-
const
|
|
7654
|
+
const destination =
|
|
7655
|
+
destinationInput === undefined
|
|
7656
|
+
? explicitDestination(path.dirname(identity.canonicalPath))
|
|
7657
|
+
: destinationFor(getProjectDir(), destinationInput, storage);
|
|
7658
|
+
let sessionPath = identity.canonicalPath;
|
|
7659
|
+
if (destination.kind === "managed" && storage instanceof FileSessionStorage) {
|
|
7660
|
+
try {
|
|
7661
|
+
sessionPath = await SessionManager.prepareManagedCandidateForWrite(sessionPath, migrationPolicy);
|
|
7662
|
+
} catch (error) {
|
|
7663
|
+
if (error instanceof SessionMigrationPolicyError)
|
|
7664
|
+
return { kind: "error", reason: "legacy_migration_disabled" };
|
|
7665
|
+
throw error;
|
|
7666
|
+
}
|
|
7667
|
+
}
|
|
7668
|
+
const inspected = inspectResumeSessionFile(sessionPath, storage);
|
|
5539
7669
|
if ("kind" in inspected) return inspected;
|
|
5540
|
-
if (
|
|
7670
|
+
if (
|
|
7671
|
+
(sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) ||
|
|
7672
|
+
(sessionPath !== identity.canonicalPath &&
|
|
7673
|
+
(identity.sessionId !== inspected.identity.sessionId || identity.sha256 !== inspected.identity.sha256))
|
|
7674
|
+
) {
|
|
5541
7675
|
return { kind: "error", reason: "identity-mismatch" };
|
|
5542
7676
|
}
|
|
5543
|
-
const entries =
|
|
7677
|
+
const entries = inspected.entries;
|
|
5544
7678
|
const header = entries[0] as SessionHeader;
|
|
5545
|
-
const dir =
|
|
5546
|
-
const manager = new SessionManager(header.cwd || getProjectDir(), dir, true, storage);
|
|
5547
|
-
await manager.#hydrateExistingSession(
|
|
5548
|
-
const ownershipInspection = inspectResumeSessionFile(
|
|
7679
|
+
const dir = destination.directory;
|
|
7680
|
+
const manager = new SessionManager(header.cwd || getProjectDir(), dir, true, storage, destination);
|
|
7681
|
+
await manager.#hydrateExistingSession(sessionPath, entries, inspected.migrationApplied);
|
|
7682
|
+
const ownershipInspection = inspectResumeSessionFile(sessionPath, storage);
|
|
5549
7683
|
if ("kind" in ownershipInspection) {
|
|
5550
7684
|
await manager.close();
|
|
5551
7685
|
return ownershipInspection;
|
|
5552
7686
|
}
|
|
5553
|
-
if (!sameResumeIdentity(identity, ownershipInspection.identity)) {
|
|
7687
|
+
if (!sameResumeIdentity(inspected.identity, ownershipInspection.identity)) {
|
|
5554
7688
|
await manager.close();
|
|
5555
7689
|
return { kind: "error", reason: "identity-mismatch" };
|
|
5556
7690
|
}
|
|
5557
|
-
|
|
7691
|
+
try {
|
|
7692
|
+
await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
|
|
7693
|
+
writeTerminalBreadcrumb(manager.cwd, sessionPath);
|
|
7694
|
+
} catch (error) {
|
|
7695
|
+
await manager.close();
|
|
7696
|
+
throw error;
|
|
7697
|
+
}
|
|
5558
7698
|
return { kind: "opened", manager };
|
|
5559
7699
|
}
|
|
5560
7700
|
|
|
@@ -5565,28 +7705,48 @@ export class SessionManager {
|
|
|
5565
7705
|
*/
|
|
5566
7706
|
static async continueRecent(
|
|
5567
7707
|
cwd: string,
|
|
5568
|
-
|
|
7708
|
+
destinationInput?: SessionDestinationInput,
|
|
5569
7709
|
storage: SessionStorage = new FileSessionStorage(),
|
|
7710
|
+
migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
|
|
5570
7711
|
): Promise<SessionManager> {
|
|
5571
|
-
const
|
|
5572
|
-
|
|
7712
|
+
const destination = destinationFor(cwd, destinationInput, storage);
|
|
7713
|
+
const dir = destination.directory;
|
|
7714
|
+
const openSelectedStrictly = async (selectedPath: string): Promise<SessionManager | undefined> => {
|
|
7715
|
+
const inspected = await SessionManager.inspectSessionTailReadOnly(selectedPath, storage);
|
|
7716
|
+
if (inspected.kind === "error") return undefined;
|
|
7717
|
+
const opened = await SessionManager.openExistingStrict(
|
|
7718
|
+
inspected.identity,
|
|
7719
|
+
destination,
|
|
7720
|
+
storage,
|
|
7721
|
+
migrationPolicy,
|
|
7722
|
+
);
|
|
7723
|
+
if (opened.kind === "error") {
|
|
7724
|
+
if (opened.reason === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
|
|
7725
|
+
return undefined;
|
|
7726
|
+
}
|
|
7727
|
+
return opened.manager;
|
|
7728
|
+
};
|
|
7729
|
+
// Legacy paths are candidates only: the managed scan validates each transcript
|
|
7730
|
+
// header cwd before it can affect default resume authority.
|
|
7731
|
+
const managedCandidates =
|
|
7732
|
+
destination.kind === "explicit"
|
|
7733
|
+
? []
|
|
7734
|
+
: await SessionManager.listForResumePickerReadOnly(cwd, destination.directory, storage);
|
|
5573
7735
|
const terminalSession = await readTerminalBreadcrumb(cwd);
|
|
5574
|
-
|
|
7736
|
+
if (terminalSession) {
|
|
7737
|
+
const opened = await openSelectedStrictly(terminalSession);
|
|
7738
|
+
if (opened) return opened;
|
|
7739
|
+
}
|
|
7740
|
+
for (const candidate of managedCandidates) {
|
|
7741
|
+
const opened = await openSelectedStrictly(candidate.path);
|
|
7742
|
+
if (opened) return opened;
|
|
7743
|
+
}
|
|
7744
|
+
const mostRecent = await findMostRecentSession(dir, storage);
|
|
5575
7745
|
if (mostRecent) {
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
// the invocation cwd; binding the manager (and HUD) to `cwd` would leave
|
|
5579
|
-
// it on the main checkout instead of the worktree it was created in.
|
|
5580
|
-
const header = (await loadEntriesFromFile(mostRecent, storage)).find(e => e.type === "session") as
|
|
5581
|
-
| SessionHeader
|
|
5582
|
-
| undefined;
|
|
5583
|
-
const resumeCwd = header?.cwd || cwd;
|
|
5584
|
-
const resumeDir = sessionDir ?? path.resolve(mostRecent, "..");
|
|
5585
|
-
const manager = new SessionManager(resumeCwd, resumeDir, true, storage);
|
|
5586
|
-
await manager.#initSessionFile(mostRecent);
|
|
5587
|
-
return manager;
|
|
7746
|
+
const opened = await openSelectedStrictly(mostRecent);
|
|
7747
|
+
if (opened) return opened;
|
|
5588
7748
|
}
|
|
5589
|
-
const manager = new SessionManager(cwd, dir, true, storage);
|
|
7749
|
+
const manager = new SessionManager(cwd, dir, true, storage, destination);
|
|
5590
7750
|
manager.#initNewSession();
|
|
5591
7751
|
return manager;
|
|
5592
7752
|
}
|
|
@@ -5611,11 +7771,10 @@ export class SessionManager {
|
|
|
5611
7771
|
sessionDir?: string,
|
|
5612
7772
|
storage: SessionStorage = new FileSessionStorage(),
|
|
5613
7773
|
): Promise<SessionInfo[]> {
|
|
5614
|
-
|
|
7774
|
+
if (!sessionDir) return await SessionManager.listForResumePickerReadOnly(cwd, undefined, storage);
|
|
5615
7775
|
try {
|
|
5616
|
-
await recoverOrphanedBackups(
|
|
5617
|
-
|
|
5618
|
-
return await collectSessionsFromFiles(files, storage);
|
|
7776
|
+
await recoverOrphanedBackups(sessionDir, storage);
|
|
7777
|
+
return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
|
|
5619
7778
|
} catch {
|
|
5620
7779
|
return [];
|
|
5621
7780
|
}
|
|
@@ -5625,12 +7784,39 @@ export class SessionManager {
|
|
|
5625
7784
|
* List all sessions across all project directories.
|
|
5626
7785
|
*/
|
|
5627
7786
|
static async listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
|
|
7787
|
+
if (!(storage instanceof FileSessionStorage)) return [];
|
|
5628
7788
|
const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
|
|
5629
7789
|
try {
|
|
5630
|
-
const
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
7790
|
+
const directories = await fs.promises.readdir(sessionsRoot, { withFileTypes: true });
|
|
7791
|
+
const seedFiles = directories
|
|
7792
|
+
.filter(entry => entry.isDirectory())
|
|
7793
|
+
.flatMap(entry => storage.listFilesSync(path.join(sessionsRoot, entry.name), "*.jsonl"));
|
|
7794
|
+
const seedSessions = await collectSessionsFromFiles(seedFiles, storage);
|
|
7795
|
+
const logicalFiles = new Set<string>();
|
|
7796
|
+
for (const cwd of new Set(seedSessions.map(session => session.cwd).filter(Boolean))) {
|
|
7797
|
+
const scope = resolveManagedScope({
|
|
7798
|
+
cwd,
|
|
7799
|
+
agentDir: path.resolve(sessionsRoot, ".."),
|
|
7800
|
+
sessionsRoot,
|
|
7801
|
+
});
|
|
7802
|
+
if (scope.kind !== "resolved") {
|
|
7803
|
+
logger.warn("Skipped invalid managed session scope during global listing", { message: scope.message });
|
|
7804
|
+
continue;
|
|
7805
|
+
}
|
|
7806
|
+
const listing = listManagedCandidates(scope.scope);
|
|
7807
|
+
if (listing.kind !== "complete") {
|
|
7808
|
+
logger.warn("Skipped unreadable managed session scope during global listing", {
|
|
7809
|
+
message: listing.message,
|
|
7810
|
+
});
|
|
7811
|
+
continue;
|
|
7812
|
+
}
|
|
7813
|
+
if (listing.invalid.length > 0)
|
|
7814
|
+
logger.warn("Ignored invalid managed session candidates during global listing", {
|
|
7815
|
+
count: listing.invalid.length,
|
|
7816
|
+
});
|
|
7817
|
+
for (const candidate of listing.owned) logicalFiles.add(candidate.path);
|
|
7818
|
+
}
|
|
7819
|
+
return await collectSessionsFromFiles([...logicalFiles], storage);
|
|
5634
7820
|
} catch {
|
|
5635
7821
|
return [];
|
|
5636
7822
|
}
|