@sayknow-cli/coding-agent 0.4.6 → 0.5.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 +112 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +9 -1
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-contract.d.ts +40 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +156 -10
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +33 -2
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +5 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +13 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
- package/dist/types/runtime-mcp/manager.d.ts +9 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +52 -4
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/query/handlers.d.ts +14 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +9 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +83 -24
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
- package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +89 -18
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/skc-runtime/state-writer.d.ts +5 -1
- package/dist/types/skc-runtime/team-launch.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
- package/dist/types/skc-runtime/team-store.d.ts +12 -0
- package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +133 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +22 -9
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +7 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -12
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/compile-args.ts +0 -1
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-docs-index.ts +4 -1
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +58 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +16 -100
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +6 -38
- package/src/cli/read-cli.ts +2 -1
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +133 -11
- package/src/commands/daemon.ts +22 -5
- package/src/commands/deep-interview.ts +26 -1
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +134 -17
- package/src/commands/team.ts +1 -1
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-contract.ts +174 -0
- package/src/config/model-profiles.ts +51 -10
- package/src/config/model-registry.ts +24 -4
- package/src/config/model-resolver.ts +2 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +157 -10
- package/src/config/settings.ts +254 -108
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
- package/src/defaults/skc/skills/team/SKILL.md +1 -1
- package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/eval/py/executor.ts +37 -13
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/extensibility/shared-events.ts +5 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/internal-urls/docs-index.generated.ts +17 -16
- package/src/internal-urls/local-protocol.ts +87 -25
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +120 -42
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +52 -0
- package/src/modes/acp/acp-agent.ts +647 -83
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/sayknow-pet-widget.ts +27 -8
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +26 -8
- package/src/modes/components/welcome.ts +50 -23
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +101 -54
- package/src/modes/controllers/event-controller.ts +119 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +100 -8
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +201 -60
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +6 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +99 -4
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +17 -3
- package/src/prompts/agents/critic.md +17 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -3
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
- package/src/runtime-mcp/json-rpc.ts +28 -11
- package/src/runtime-mcp/manager.ts +77 -59
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +293 -43
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +22 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +10 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +793 -121
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +87 -25
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/prompt-reconciliation.ts +240 -0
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
- package/src/sdk/bus/telegram-daemon-control.ts +161 -40
- package/src/sdk/bus/telegram-daemon.ts +3649 -593
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +8 -2
- package/src/sdk/host/control/operations.ts +3 -2
- package/src/sdk/host/host.ts +27 -9
- package/src/sdk/host/query/handlers.ts +125 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +145 -0
- package/src/sdk/protocol/operation-registry.ts +24 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +69 -8
- package/src/sdk/startup-capability.ts +15 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1112 -314
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +1426 -259
- package/src/session/internal/managed-session-storage.ts +414 -238
- package/src/session/internal/native-publish-outcome.ts +271 -0
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2079 -469
- package/src/session/session-storage.ts +151 -11
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +49 -5
- package/src/setup/provider-presets.json +2 -1
- package/src/skc-runtime/deep-interview-runtime.ts +303 -36
- package/src/skc-runtime/deep-interview-stage.ts +1016 -0
- package/src/skc-runtime/deep-interview-state.ts +8 -5
- package/src/skc-runtime/gc-render.ts +1 -0
- package/src/skc-runtime/gc-runtime.ts +11 -1
- package/src/skc-runtime/linux-proc.ts +70 -36
- package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/skc-runtime/ralplan-runtime.ts +1057 -75
- package/src/skc-runtime/repository-binding.ts +267 -0
- package/src/skc-runtime/session-state-sidecar.ts +25 -9
- package/src/skc-runtime/state-runtime.ts +50 -12
- package/src/skc-runtime/state-writer.ts +68 -56
- package/src/skc-runtime/team-launch.ts +34 -5
- package/src/skc-runtime/team-runtime.ts +41 -1
- package/src/skc-runtime/team-store.ts +32 -0
- package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/skc-runtime/team-workers.ts +1 -0
- package/src/skc-runtime/tmux-common.ts +13 -2
- package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
- package/src/skc-runtime/tmux-sessions.ts +45 -6
- package/src/skc-runtime/ultragoal-guard.ts +93 -15
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
- package/src/skc-runtime/ultragoal-runtime.ts +469 -38
- package/src/skc-runtime/workflow-manifest.generated.json +195 -5
- package/src/skc-runtime/workflow-manifest.ts +67 -3
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +104 -12
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/executor.ts +165 -47
- package/src/task/index.ts +123 -15
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/receipt.ts +3 -0
- package/src/task/render.ts +45 -26
- package/src/task/skc-command.ts +2 -2
- package/src/task/types.ts +55 -10
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +127 -154
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +7 -1
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1544 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
|
@@ -3,28 +3,42 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import * as native from "@sayknow-cli/natives";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
canonicalExistingDirectoryIdentity,
|
|
8
|
+
verifyOwnerOnlyPathSecurity,
|
|
9
|
+
verifyOwnerOnlyPathSecurityExpected,
|
|
10
|
+
} from "@sayknow-cli/natives";
|
|
11
|
+
import { logger, pathIsWithin } from "@sayknow-cli/utils";
|
|
8
12
|
import type { ResumeSessionIdentity } from "../session-manager";
|
|
9
13
|
import {
|
|
10
14
|
FileSessionStorage,
|
|
11
15
|
type NativeDirectoryTreeSnapshot,
|
|
12
16
|
type SessionStorageFileIdentity,
|
|
17
|
+
type VerifiedSessionDeleteResult,
|
|
13
18
|
} from "../session-storage";
|
|
14
19
|
import {
|
|
15
20
|
acquireManagedLock,
|
|
21
|
+
assertManagedDirectoryRoot,
|
|
16
22
|
captureManagedFileNoFollow,
|
|
17
23
|
captureManagedFilePrefixNoFollow,
|
|
18
24
|
copyManagedFileNoReplace,
|
|
19
25
|
ensureManagedDirectory,
|
|
20
26
|
fsyncManagedArtifactTree,
|
|
27
|
+
MANAGED_ARTIFACT_COPY_BATCH_SIZE,
|
|
28
|
+
MANAGED_ARTIFACT_MAX_FILES,
|
|
29
|
+
MANAGED_ARTIFACT_MAX_TOTAL_BYTES,
|
|
30
|
+
type ManagedDirectoryRoot,
|
|
31
|
+
type ManagedFileSnapshot,
|
|
32
|
+
ManagedPublishError,
|
|
21
33
|
ManagedSessionDescendantStore,
|
|
34
|
+
type ManagedSessionSecurityPolicy,
|
|
22
35
|
type ManagedStorageLock,
|
|
23
|
-
|
|
36
|
+
prepareManagedDirectoryRoot,
|
|
24
37
|
publishManagedFileNoReplace,
|
|
25
38
|
publishManagedTombstone,
|
|
26
39
|
retainManagedDirectoryAuthority,
|
|
27
40
|
validateManagedArtifactTree,
|
|
41
|
+
validateNativeSecurityResult,
|
|
28
42
|
} from "./managed-session-storage";
|
|
29
43
|
|
|
30
44
|
export const MANAGED_SESSION_LAYOUT_VERSION = 2 as const;
|
|
@@ -44,9 +58,52 @@ export interface ManagedScope {
|
|
|
44
58
|
platform: "posix" | "win32";
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Opaque managed writer authority captured by a trusted destination. The open
|
|
63
|
+
* transaction must use this authority rather than reacquiring its root from a
|
|
64
|
+
* pathname after resume inspection.
|
|
65
|
+
*/
|
|
66
|
+
export interface ManagedCandidateWriteAuthority {
|
|
67
|
+
readonly rootAuthority: ManagedDirectoryRoot;
|
|
68
|
+
readonly retainedAuthority?: native.RecoveryFsRoot;
|
|
69
|
+
readonly retainedDirectory?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof prepareManagedDirectoryRoot>>();
|
|
48
73
|
const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
|
|
49
74
|
const managedDirectoryAuthorities = new WeakMap<ManagedScope, native.RecoveryFsRoot | undefined>();
|
|
75
|
+
const boundManagedWriteAuthorities = new WeakMap<ManagedScope, ManagedCandidateWriteAuthority>();
|
|
76
|
+
|
|
77
|
+
function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandidateWriteAuthority): void {
|
|
78
|
+
if (
|
|
79
|
+
authority.retainedAuthority &&
|
|
80
|
+
authority.retainedDirectory !== undefined &&
|
|
81
|
+
path.resolve(authority.retainedDirectory) === path.resolve(scope.directoryPath)
|
|
82
|
+
) {
|
|
83
|
+
new ManagedSessionDescendantStore(authority.rootAuthority, scope.directoryPath, {
|
|
84
|
+
authority: authority.retainedAuthority,
|
|
85
|
+
authorityBaseDir: scope.directoryPath,
|
|
86
|
+
}).assertBound();
|
|
87
|
+
} else {
|
|
88
|
+
assertManagedDirectoryRoot(authority.rootAuthority);
|
|
89
|
+
}
|
|
90
|
+
managedRoots.set(scope, authority.rootAuthority);
|
|
91
|
+
boundManagedWriteAuthorities.set(scope, authority);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A prepared scope is a retained authority boundary, not a path that may be re-adopted. */
|
|
95
|
+
function assertRetainedManagedDirectoryIdentity(scope: ManagedScope): void {
|
|
96
|
+
const expected = managedDirectoryIdentities.get(scope);
|
|
97
|
+
if (!expected) return;
|
|
98
|
+
const current = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
99
|
+
if (
|
|
100
|
+
!current.isDirectory() ||
|
|
101
|
+
current.isSymbolicLink() ||
|
|
102
|
+
current.dev !== expected.dev ||
|
|
103
|
+
current.ino !== expected.ino
|
|
104
|
+
)
|
|
105
|
+
throw new Error("Managed session directory changed");
|
|
106
|
+
}
|
|
50
107
|
|
|
51
108
|
export function managedDirectoryAuthorityForScope(scope: ManagedScope): native.RecoveryFsRoot | undefined {
|
|
52
109
|
if (!managedDirectoryAuthorities.has(scope)) throw new Error("Managed session directory authority was not prepared");
|
|
@@ -61,24 +118,32 @@ export function managedDirectoryIdentityForScope(scope: ManagedScope): { dev: bi
|
|
|
61
118
|
|
|
62
119
|
function configuredRootPath(scope: ManagedScope): string {
|
|
63
120
|
let candidate = pathIsWithin(scope.agentDir, scope.sessionsRoot) ? scope.agentDir : path.dirname(scope.sessionsRoot);
|
|
121
|
+
const suffix: string[] = [];
|
|
64
122
|
for (;;) {
|
|
65
123
|
try {
|
|
66
124
|
const stat = fs.lstatSync(candidate);
|
|
67
125
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe configured root: ${candidate}`);
|
|
68
|
-
|
|
126
|
+
const canonical = fs.realpathSync.native(candidate);
|
|
127
|
+
return suffix.length === 0 ? canonical : path.join(canonical, ...suffix);
|
|
69
128
|
} catch (error) {
|
|
70
129
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
71
130
|
const parent = path.dirname(candidate);
|
|
72
131
|
if (parent === candidate) throw new Error("Configured managed root is unavailable.");
|
|
132
|
+
suffix.unshift(path.basename(candidate));
|
|
73
133
|
candidate = parent;
|
|
74
134
|
}
|
|
75
135
|
}
|
|
76
136
|
}
|
|
77
137
|
|
|
78
|
-
function scopeRoot(scope: ManagedScope) {
|
|
138
|
+
function scopeRoot(scope: ManagedScope, policy: ManagedSessionSecurityPolicy = "default") {
|
|
139
|
+
const bound = boundManagedWriteAuthorities.get(scope);
|
|
140
|
+
if (bound) {
|
|
141
|
+
bindManagedWriteAuthority(scope, bound);
|
|
142
|
+
return bound.rootAuthority;
|
|
143
|
+
}
|
|
79
144
|
const retained = managedRoots.get(scope);
|
|
80
145
|
if (retained) return retained;
|
|
81
|
-
const root =
|
|
146
|
+
const root = prepareManagedDirectoryRoot(configuredRootPath(scope), policy);
|
|
82
147
|
managedRoots.set(scope, root);
|
|
83
148
|
return root;
|
|
84
149
|
}
|
|
@@ -96,11 +161,20 @@ export type ManagedScopeErrorCode =
|
|
|
96
161
|
| "network_unsupported"
|
|
97
162
|
| "sessions_root_unavailable"
|
|
98
163
|
| "binding_conflict"
|
|
99
|
-
| "binding_invalid"
|
|
164
|
+
| "binding_invalid"
|
|
165
|
+
| "atomic_unavailable"
|
|
166
|
+
| "invalid_request"
|
|
167
|
+
| "durability_failed"
|
|
168
|
+
| "durability_not_provable";
|
|
100
169
|
|
|
101
170
|
export type ManagedScopeResolution =
|
|
102
171
|
| { kind: "resolved"; scope: ManagedScope }
|
|
103
|
-
| {
|
|
172
|
+
| {
|
|
173
|
+
kind: "error";
|
|
174
|
+
code: ManagedScopeErrorCode;
|
|
175
|
+
message: string;
|
|
176
|
+
cause?: { readonly classification: string; readonly diagnostic?: string };
|
|
177
|
+
};
|
|
104
178
|
|
|
105
179
|
export interface ManagedCandidate {
|
|
106
180
|
sessionId: string;
|
|
@@ -128,7 +202,11 @@ export type ManagedOpenFailure =
|
|
|
128
202
|
| "destination_conflict"
|
|
129
203
|
| "source_changed"
|
|
130
204
|
| "unsafe_artifacts"
|
|
205
|
+
| "artifact_capacity_exceeded"
|
|
131
206
|
| "durability_failed"
|
|
207
|
+
| "atomic_unavailable"
|
|
208
|
+
| "invalid_request"
|
|
209
|
+
| "durability_not_provable"
|
|
132
210
|
| "migration_retired"
|
|
133
211
|
| "legacy_migration_disabled"
|
|
134
212
|
| "managed_storage_unsupported";
|
|
@@ -140,6 +218,7 @@ export type ManagedOpenCandidateResult =
|
|
|
140
218
|
export type ManagedDeleteCandidateResult =
|
|
141
219
|
| { kind: "deleted"; tombstonePath: string }
|
|
142
220
|
| { kind: "already_deleted"; tombstonePath: string }
|
|
221
|
+
| { kind: "cleanup_pending"; tombstonePath: string; phase: "artifacts" | "transcript"; message: string }
|
|
143
222
|
| { kind: "error"; code: ManagedOpenFailure; message: string };
|
|
144
223
|
|
|
145
224
|
type NativeIdentity =
|
|
@@ -155,8 +234,6 @@ type NativeIdentityFailureCode =
|
|
|
155
234
|
| "identity_unavailable"
|
|
156
235
|
| "io_error";
|
|
157
236
|
|
|
158
|
-
type NativeSecurity = { ok: true } | { ok: false; code: string };
|
|
159
|
-
|
|
160
237
|
interface Binding {
|
|
161
238
|
schemaVersion: 1;
|
|
162
239
|
layoutVersion: 2;
|
|
@@ -196,6 +273,22 @@ function identityFor(cwd: string): NativeIdentity {
|
|
|
196
273
|
return canonicalExistingDirectoryIdentity(cwd) as NativeIdentity;
|
|
197
274
|
}
|
|
198
275
|
|
|
276
|
+
function verifyExistingManagedScopeDirectory(pathname: string) {
|
|
277
|
+
if (process.platform !== "win32") return verifyOwnerOnlyPathSecurity(pathname, "directory");
|
|
278
|
+
const expected = fs.lstatSync(pathname, { bigint: true });
|
|
279
|
+
if (!expected.isDirectory() || expected.isSymbolicLink()) throw new Error("Unsafe managed directory");
|
|
280
|
+
const verified = verifyOwnerOnlyPathSecurityExpected(pathname, "directory", expected.dev, expected.ino);
|
|
281
|
+
const current = fs.lstatSync(pathname, { bigint: true });
|
|
282
|
+
if (
|
|
283
|
+
!current.isDirectory() ||
|
|
284
|
+
current.isSymbolicLink() ||
|
|
285
|
+
current.dev !== expected.dev ||
|
|
286
|
+
current.ino !== expected.ino
|
|
287
|
+
)
|
|
288
|
+
throw new Error("Managed session directory changed");
|
|
289
|
+
return verified;
|
|
290
|
+
}
|
|
291
|
+
|
|
199
292
|
function canonicalExistingPathForIo(base: string, identity: CanonicalNativeIdentity): string {
|
|
200
293
|
if (identity.platform !== "win32") return identity.canonicalPath;
|
|
201
294
|
try {
|
|
@@ -317,11 +410,16 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
|
|
|
317
410
|
return validateBindingRaw(scope, raw);
|
|
318
411
|
}
|
|
319
412
|
|
|
320
|
-
|
|
413
|
+
interface ManagedScopeInput {
|
|
321
414
|
cwd: string;
|
|
322
415
|
agentDir: string;
|
|
323
416
|
sessionsRoot: string;
|
|
324
|
-
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function resolveManagedScopeInternal(
|
|
420
|
+
input: ManagedScopeInput,
|
|
421
|
+
allowRepairableAclFailure: boolean,
|
|
422
|
+
): ManagedScopeResolution {
|
|
325
423
|
const identity = identityFor(input.cwd);
|
|
326
424
|
if (!identity.ok) return nativeFailure(identity.code);
|
|
327
425
|
try {
|
|
@@ -379,13 +477,18 @@ export function resolveManagedScope(input: {
|
|
|
379
477
|
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
380
478
|
return { kind: "error", code: "binding_invalid", message: "The managed scope path is not a safe directory." };
|
|
381
479
|
}
|
|
382
|
-
const security =
|
|
383
|
-
|
|
480
|
+
const security = validateNativeSecurityResult(
|
|
481
|
+
verifyExistingManagedScopeDirectory(scope.directoryPath),
|
|
482
|
+
"verify",
|
|
483
|
+
"directory",
|
|
484
|
+
);
|
|
485
|
+
if (!security.ok && (!allowRepairableAclFailure || security.code !== "acl_verify_failed")) {
|
|
384
486
|
return {
|
|
385
487
|
kind: "error",
|
|
386
488
|
code: "binding_invalid",
|
|
387
489
|
message: "The managed scope security could not be verified.",
|
|
388
490
|
};
|
|
491
|
+
}
|
|
389
492
|
} catch (error) {
|
|
390
493
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
391
494
|
return { kind: "error", code: "binding_invalid", message: "The managed scope path could not be inspected." };
|
|
@@ -394,6 +497,15 @@ export function resolveManagedScope(input: {
|
|
|
394
497
|
return validateExistingBinding(scope) ?? { kind: "resolved", scope };
|
|
395
498
|
}
|
|
396
499
|
|
|
500
|
+
export function resolveManagedScope(input: ManagedScopeInput): ManagedScopeResolution {
|
|
501
|
+
return resolveManagedScopeInternal(input, false);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Resolve a scope for a synchronous write without mutating an existing ACL mismatch. */
|
|
505
|
+
export function resolveManagedScopeForWrite(input: ManagedScopeInput): ManagedScopeResolution {
|
|
506
|
+
return resolveManagedScopeInternal(input, true);
|
|
507
|
+
}
|
|
508
|
+
|
|
397
509
|
function legacyDirectoryNames(
|
|
398
510
|
platform: ManagedScope["platform"],
|
|
399
511
|
canonicalCwd: string,
|
|
@@ -461,6 +573,63 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
461
573
|
}
|
|
462
574
|
}
|
|
463
575
|
|
|
576
|
+
export function canonicalBindingOpenFlags(platform: NodeJS.Platform = process.platform): number {
|
|
577
|
+
return (platform === "win32" ? fs.constants.O_RDWR : fs.constants.O_RDONLY) | fs.constants.O_NOFOLLOW;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function fsyncCanonicalBinding(bindingPath: string, expected: string): void {
|
|
581
|
+
let captured: ManagedFileSnapshot;
|
|
582
|
+
try {
|
|
583
|
+
captured = captureManagedFileNoFollow(bindingPath);
|
|
584
|
+
} catch {
|
|
585
|
+
throw new Error("binding_invalid");
|
|
586
|
+
}
|
|
587
|
+
if (captured.bytes.toString("utf8") !== expected) throw new Error("binding_invalid");
|
|
588
|
+
let descriptor: number | undefined;
|
|
589
|
+
try {
|
|
590
|
+
// Bun on Windows rejects fsync on a read-only file descriptor with EPERM.
|
|
591
|
+
// The managed binding is owner-writable, so reopen it read/write only for the durability fence.
|
|
592
|
+
descriptor = fs.openSync(bindingPath, canonicalBindingOpenFlags());
|
|
593
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
594
|
+
if (
|
|
595
|
+
before.dev !== captured.identity.dev ||
|
|
596
|
+
before.ino !== captured.identity.ino ||
|
|
597
|
+
before.size !== BigInt(captured.identity.size) ||
|
|
598
|
+
before.mtimeNs !== captured.identity.mtimeNs
|
|
599
|
+
)
|
|
600
|
+
throw new Error("binding_invalid");
|
|
601
|
+
fs.fsyncSync(descriptor);
|
|
602
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
603
|
+
if (
|
|
604
|
+
after.dev !== captured.identity.dev ||
|
|
605
|
+
after.ino !== captured.identity.ino ||
|
|
606
|
+
after.size !== BigInt(captured.identity.size) ||
|
|
607
|
+
after.mtimeNs !== captured.identity.mtimeNs
|
|
608
|
+
)
|
|
609
|
+
throw new Error("binding_invalid");
|
|
610
|
+
} catch (error) {
|
|
611
|
+
if (error instanceof Error && error.message === "binding_invalid") throw error;
|
|
612
|
+
throw new Error("durability_failed", { cause: error });
|
|
613
|
+
} finally {
|
|
614
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
615
|
+
}
|
|
616
|
+
fsyncManagedParent(bindingPath);
|
|
617
|
+
let recaptured: ManagedFileSnapshot;
|
|
618
|
+
try {
|
|
619
|
+
recaptured = captureManagedFileNoFollow(bindingPath);
|
|
620
|
+
} catch {
|
|
621
|
+
throw new Error("binding_invalid");
|
|
622
|
+
}
|
|
623
|
+
if (
|
|
624
|
+
recaptured.bytes.toString("utf8") !== expected ||
|
|
625
|
+
recaptured.identity.dev !== captured.identity.dev ||
|
|
626
|
+
recaptured.identity.ino !== captured.identity.ino ||
|
|
627
|
+
recaptured.identity.size !== captured.identity.size ||
|
|
628
|
+
recaptured.identity.mtimeNs !== captured.identity.mtimeNs
|
|
629
|
+
)
|
|
630
|
+
throw new Error("binding_invalid");
|
|
631
|
+
}
|
|
632
|
+
|
|
464
633
|
type CandidatePreflight =
|
|
465
634
|
| { kind: "capture"; identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint } }
|
|
466
635
|
| {
|
|
@@ -594,46 +763,159 @@ function listDirectoryCandidates(
|
|
|
594
763
|
});
|
|
595
764
|
}
|
|
596
765
|
|
|
597
|
-
export async function ensureManagedScope(
|
|
766
|
+
export async function ensureManagedScope(
|
|
767
|
+
scope: ManagedScope,
|
|
768
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
769
|
+
): Promise<ManagedScopeResolution> {
|
|
598
770
|
try {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
ensureManagedDirectory(scope.
|
|
771
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
772
|
+
const root = scopeRoot(scope, policy);
|
|
773
|
+
ensureManagedDirectory(scope.sessionsRoot, root, policy);
|
|
774
|
+
ensureManagedDirectory(scope.directoryPath, root, policy);
|
|
602
775
|
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
603
776
|
const binding = `${JSON.stringify(bindingFor(scope))}\n`;
|
|
777
|
+
let bindingCollision = false;
|
|
604
778
|
try {
|
|
605
|
-
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root);
|
|
779
|
+
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
|
|
606
780
|
} catch (error) {
|
|
607
781
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
782
|
+
bindingCollision = true;
|
|
608
783
|
}
|
|
609
|
-
|
|
784
|
+
const validated = validateExistingBinding(scope);
|
|
785
|
+
if (validated) return validated;
|
|
786
|
+
if (bindingCollision) fsyncCanonicalBinding(bindingPath, binding);
|
|
787
|
+
|
|
788
|
+
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
789
|
+
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
790
|
+
throw new Error("Managed session directory changed");
|
|
791
|
+
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
792
|
+
return { kind: "resolved", scope };
|
|
610
793
|
} catch (error) {
|
|
794
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
795
|
+
const message =
|
|
796
|
+
publication?.classification ??
|
|
797
|
+
(error instanceof Error ? error.message : "The managed scope could not be initialized.");
|
|
798
|
+
const code =
|
|
799
|
+
message === "atomic_unavailable" ||
|
|
800
|
+
message === "invalid_request" ||
|
|
801
|
+
message === "durability_failed" ||
|
|
802
|
+
message === "durability_not_provable"
|
|
803
|
+
? message
|
|
804
|
+
: "binding_invalid";
|
|
805
|
+
|
|
611
806
|
return {
|
|
612
807
|
kind: "error",
|
|
613
|
-
code
|
|
614
|
-
message
|
|
808
|
+
code,
|
|
809
|
+
message,
|
|
810
|
+
...(publication
|
|
811
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
812
|
+
: { cause: { classification: code } }),
|
|
615
813
|
};
|
|
616
814
|
}
|
|
617
815
|
}
|
|
618
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Re-apply owner-only security to every descendant of a managed scope directory.
|
|
819
|
+
*
|
|
820
|
+
* A managed scope can accumulate group/other-readable descendants when a
|
|
821
|
+
* different code path writes into it without the secured managed-storage
|
|
822
|
+
* helpers — notably the resident-cache `EphemeralBlobStore` created on the
|
|
823
|
+
* explicit session path. The managed-tree snapshot fails closed on the first
|
|
824
|
+
* such descendant (`mode_mismatch`), which would otherwise abort launch with an
|
|
825
|
+
* uncaught exception. Re-securing the tree in place lets a drifted scope
|
|
826
|
+
* recover on the next launch instead of trapping the user behind a fatal error.
|
|
827
|
+
*/
|
|
828
|
+
function reapplyOwnerOnlyManagedTree(directory: string): void {
|
|
829
|
+
let entries: fs.Dirent[];
|
|
830
|
+
try {
|
|
831
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
832
|
+
} catch {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
for (const entry of entries) {
|
|
836
|
+
const child = path.join(directory, entry.name);
|
|
837
|
+
let stat: fs.Stats;
|
|
838
|
+
try {
|
|
839
|
+
stat = fs.lstatSync(child);
|
|
840
|
+
} catch {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (stat.isSymbolicLink()) continue;
|
|
844
|
+
if (stat.isDirectory()) {
|
|
845
|
+
reapplyOwnerOnlyManagedTree(child);
|
|
846
|
+
try {
|
|
847
|
+
native.applyOwnerOnlyPathSecurity(child, "directory");
|
|
848
|
+
} catch {
|
|
849
|
+
// Best-effort: the managed-tree snapshot re-verifies and reports genuine failures.
|
|
850
|
+
}
|
|
851
|
+
} else if (stat.isFile()) {
|
|
852
|
+
try {
|
|
853
|
+
native.applyOwnerOnlyPathSecurity(child, "file");
|
|
854
|
+
} catch {
|
|
855
|
+
// Best-effort, as above.
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
native.applyOwnerOnlyPathSecurity(directory, "directory");
|
|
861
|
+
} catch {
|
|
862
|
+
// Best-effort, as above.
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* True when a managed setup error reflects a fixable owner-only *mode* drift
|
|
868
|
+
* (group/other permission bits) rather than an ownership or identity change.
|
|
869
|
+
* Only mode drift can be self-healed by re-applying owner-only permissions.
|
|
870
|
+
*/
|
|
871
|
+
function isRecoverableOwnerOnlyModeDrift(error: unknown): boolean {
|
|
872
|
+
const message = error instanceof Error ? error.message : "";
|
|
873
|
+
return message === "mode_mismatch" || message.endsWith(": mode_mismatch");
|
|
874
|
+
}
|
|
875
|
+
|
|
619
876
|
/** Synchronously create and validate the v2 binding before a default session writer exists. */
|
|
620
|
-
export function prepareManagedSessionScopeForWriteSync(
|
|
877
|
+
export function prepareManagedSessionScopeForWriteSync(
|
|
878
|
+
scope: ManagedScope,
|
|
879
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
880
|
+
authority?: ManagedCandidateWriteAuthority,
|
|
881
|
+
): ManagedScopeResolution {
|
|
621
882
|
try {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
883
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
884
|
+
const root = authority?.rootAuthority ?? scopeRoot(scope, policy);
|
|
885
|
+
if (authority) bindManagedWriteAuthority(scope, authority);
|
|
886
|
+
ensureManagedDirectory(scope.sessionsRoot, root, policy);
|
|
887
|
+
ensureManagedDirectory(scope.directoryPath, root, policy);
|
|
625
888
|
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
626
889
|
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
627
890
|
throw new Error("Managed session directory changed");
|
|
628
|
-
const retainedAuthority =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
891
|
+
const retainedAuthority =
|
|
892
|
+
authority?.retainedAuthority &&
|
|
893
|
+
authority.retainedDirectory !== undefined &&
|
|
894
|
+
path.resolve(authority.retainedDirectory) === path.resolve(scope.directoryPath)
|
|
895
|
+
? authority.retainedAuthority
|
|
896
|
+
: retainManagedDirectoryAuthority(root, scope.directoryPath, {
|
|
897
|
+
dev: preparedDirectory.dev,
|
|
898
|
+
ino: preparedDirectory.ino,
|
|
899
|
+
});
|
|
900
|
+
const buildStore = () =>
|
|
901
|
+
new ManagedSessionDescendantStore(
|
|
902
|
+
root,
|
|
903
|
+
scope.directoryPath,
|
|
904
|
+
retainedAuthority ? { authority: retainedAuthority, authorityBaseDir: scope.directoryPath } : undefined,
|
|
905
|
+
policy,
|
|
906
|
+
);
|
|
907
|
+
let store: ManagedSessionDescendantStore;
|
|
908
|
+
try {
|
|
909
|
+
store = buildStore();
|
|
910
|
+
} catch (error) {
|
|
911
|
+
if (process.platform === "win32" && policy === "windows-existing-verify-first") throw error;
|
|
912
|
+
if (!isRecoverableOwnerOnlyModeDrift(error)) throw error;
|
|
913
|
+
// A prior writer left group/other-readable descendants under the scope
|
|
914
|
+
// (e.g. resident-cache blobs written on the explicit session path).
|
|
915
|
+
// Re-secure the tree in place and retry once before failing closed.
|
|
916
|
+
reapplyOwnerOnlyManagedTree(scope.directoryPath);
|
|
917
|
+
store = buildStore();
|
|
918
|
+
}
|
|
637
919
|
const binding = new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`);
|
|
638
920
|
try {
|
|
639
921
|
store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
|
|
@@ -655,16 +937,32 @@ export function prepareManagedSessionScopeForWriteSync(scope: ManagedScope): Man
|
|
|
655
937
|
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
656
938
|
if (validated) return validated;
|
|
657
939
|
const internal = managedInternalDirectory(scope);
|
|
658
|
-
ensureManagedDirectory(internal, root);
|
|
659
|
-
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
|
|
660
|
-
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
|
|
661
|
-
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
|
|
940
|
+
ensureManagedDirectory(internal, root, policy);
|
|
941
|
+
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root, policy);
|
|
942
|
+
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root, policy);
|
|
943
|
+
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
|
|
662
944
|
return { kind: "resolved", scope };
|
|
663
945
|
} catch (error) {
|
|
946
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
947
|
+
const message =
|
|
948
|
+
publication?.classification ??
|
|
949
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
950
|
+
const code =
|
|
951
|
+
message === "atomic_unavailable" ||
|
|
952
|
+
message === "invalid_request" ||
|
|
953
|
+
message === "durability_failed" ||
|
|
954
|
+
message === "durability_not_provable"
|
|
955
|
+
? message
|
|
956
|
+
: "binding_invalid";
|
|
664
957
|
return {
|
|
665
958
|
kind: "error",
|
|
666
|
-
code
|
|
667
|
-
message
|
|
959
|
+
code,
|
|
960
|
+
message,
|
|
961
|
+
...(publication
|
|
962
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
963
|
+
: code === "binding_invalid"
|
|
964
|
+
? {}
|
|
965
|
+
: { cause: { classification: code } }),
|
|
668
966
|
};
|
|
669
967
|
}
|
|
670
968
|
}
|
|
@@ -787,7 +1085,11 @@ function expectedFailure(error: unknown): ManagedOpenFailure {
|
|
|
787
1085
|
message === "destination_conflict" ||
|
|
788
1086
|
message === "source_changed" ||
|
|
789
1087
|
message === "unsafe_artifacts" ||
|
|
1088
|
+
message === "artifact_capacity_exceeded" ||
|
|
790
1089
|
message === "durability_failed" ||
|
|
1090
|
+
message === "atomic_unavailable" ||
|
|
1091
|
+
message === "invalid_request" ||
|
|
1092
|
+
message === "durability_not_provable" ||
|
|
791
1093
|
message === "migration_retired"
|
|
792
1094
|
? message
|
|
793
1095
|
: "managed_storage_unsupported";
|
|
@@ -804,10 +1106,33 @@ function sameCandidate(left: ManagedCandidate, right: ManagedCandidate): boolean
|
|
|
804
1106
|
);
|
|
805
1107
|
}
|
|
806
1108
|
|
|
1109
|
+
function matchesExpectedResumeIdentity(candidate: ManagedCandidate, expected: ResumeSessionIdentity): boolean {
|
|
1110
|
+
return (
|
|
1111
|
+
path.resolve(candidate.identity.canonicalPath) === path.resolve(expected.canonicalPath) &&
|
|
1112
|
+
candidate.identity.sessionId === expected.sessionId &&
|
|
1113
|
+
candidate.identity.dev === expected.dev &&
|
|
1114
|
+
candidate.identity.ino === expected.ino &&
|
|
1115
|
+
candidate.identity.size === expected.size &&
|
|
1116
|
+
candidate.identity.mtimeMs === expected.mtimeMs &&
|
|
1117
|
+
candidate.identity.mtimeNs === expected.mtimeNs &&
|
|
1118
|
+
candidate.identity.sha256 === expected.sha256
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function revalidatePickerConsent(
|
|
1123
|
+
scope: ManagedScope,
|
|
1124
|
+
candidate: ManagedCandidate,
|
|
1125
|
+
expectedIdentity: ResumeSessionIdentity,
|
|
1126
|
+
): ManagedCandidate {
|
|
1127
|
+
const current = validateCandidateForScope(scope, candidate);
|
|
1128
|
+
if (!current || !matchesExpectedResumeIdentity(current, expectedIdentity)) throw new Error("source_changed");
|
|
1129
|
+
return current;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
807
1132
|
function receiptPathFor(
|
|
808
1133
|
scope: ManagedScope,
|
|
809
1134
|
source: ManagedCandidate,
|
|
810
|
-
state: "prepared" | "published" | "committed" = "committed",
|
|
1135
|
+
state: "prepared" | "detached" | "published" | "committed" = "committed",
|
|
811
1136
|
): string {
|
|
812
1137
|
const suffix = state === "committed" ? "" : `.${state}`;
|
|
813
1138
|
return path.join(
|
|
@@ -846,6 +1171,14 @@ function receiptMatches(
|
|
|
846
1171
|
identity?: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown };
|
|
847
1172
|
};
|
|
848
1173
|
artifactManifest?: unknown;
|
|
1174
|
+
sourceArtifactQuarantine?: { role?: unknown };
|
|
1175
|
+
sourceArtifactCleanup?: {
|
|
1176
|
+
state?: unknown;
|
|
1177
|
+
role?: unknown;
|
|
1178
|
+
retainedPath?: unknown;
|
|
1179
|
+
identity?: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown };
|
|
1180
|
+
tree?: unknown;
|
|
1181
|
+
};
|
|
849
1182
|
};
|
|
850
1183
|
const exact = (
|
|
851
1184
|
recorded: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown } | undefined,
|
|
@@ -886,12 +1219,43 @@ function receiptMatches(
|
|
|
886
1219
|
item.size >= 0
|
|
887
1220
|
);
|
|
888
1221
|
});
|
|
1222
|
+
const quarantine = record.sourceArtifactQuarantine;
|
|
1223
|
+
const cleanup = record.sourceArtifactCleanup;
|
|
1224
|
+
const cleanupIdentity = cleanup?.identity;
|
|
1225
|
+
const cleanupTree = artifactTreeSnapshot(cleanup?.tree);
|
|
1226
|
+
const validCleanup =
|
|
1227
|
+
cleanup === undefined ||
|
|
1228
|
+
(quarantine?.role === "detached_artifact_root" &&
|
|
1229
|
+
cleanup.state === "cleanup_pending" &&
|
|
1230
|
+
cleanup.role === "exchange_placeholder" &&
|
|
1231
|
+
typeof cleanup.retainedPath === "string" &&
|
|
1232
|
+
cleanupIdentity?.dev !== undefined &&
|
|
1233
|
+
cleanupIdentity.ino !== undefined &&
|
|
1234
|
+
cleanupIdentity.size !== undefined &&
|
|
1235
|
+
cleanupIdentity.mtimeNs !== undefined &&
|
|
1236
|
+
!!cleanupTree &&
|
|
1237
|
+
cleanupAuthorityMatches(
|
|
1238
|
+
{
|
|
1239
|
+
state: "cleanup_pending",
|
|
1240
|
+
role: "exchange_placeholder",
|
|
1241
|
+
retainedPath: cleanup.retainedPath,
|
|
1242
|
+
identity: {
|
|
1243
|
+
dev: BigInt(String(cleanupIdentity.dev)),
|
|
1244
|
+
ino: BigInt(String(cleanupIdentity.ino)),
|
|
1245
|
+
size: BigInt(String(cleanupIdentity.size)),
|
|
1246
|
+
mtimeNs: BigInt(String(cleanupIdentity.mtimeNs)),
|
|
1247
|
+
},
|
|
1248
|
+
tree: cleanupTree,
|
|
1249
|
+
},
|
|
1250
|
+
path.dirname(source.path),
|
|
1251
|
+
));
|
|
889
1252
|
return (
|
|
890
1253
|
record.schemaVersion === 2 &&
|
|
891
1254
|
record.state === "committed" &&
|
|
892
1255
|
record.policy === "copy-retain" &&
|
|
893
1256
|
record.scope === scopeDigest(scope.platform, scope.canonicalCwd) &&
|
|
894
1257
|
validManifest &&
|
|
1258
|
+
validCleanup &&
|
|
895
1259
|
manifestContains(destination.path, manifest as readonly ArtifactManifestEntry[]) &&
|
|
896
1260
|
record.source?.path === source.path &&
|
|
897
1261
|
record.source?.sessionId === source.sessionId &&
|
|
@@ -1034,6 +1398,65 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1034
1398
|
* a returned partial result appends `pending(N + 1)` with the observed detached
|
|
1035
1399
|
* identity/path. Restart accepts only a contiguous, target-bound sequence.
|
|
1036
1400
|
*/
|
|
1401
|
+
type RetainedArtifactsRootReceipt = {
|
|
1402
|
+
path: string;
|
|
1403
|
+
identity: SessionStorageFileIdentity;
|
|
1404
|
+
tree: NativeDirectoryTreeSnapshot;
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
|
|
1408
|
+
const pathname = deterministicRemovalRoot(receipt.plannedArtifactsPath);
|
|
1409
|
+
if (!fs.existsSync(pathname)) return undefined;
|
|
1410
|
+
if (!receipt.expectedArtifactsIdentity) throw new Error("durability_failed");
|
|
1411
|
+
const identity = artifactIdentityAt(pathname);
|
|
1412
|
+
if (!identity || !sameArtifactRootIdentity(identity, receipt.expectedArtifactsIdentity))
|
|
1413
|
+
throw new Error("durability_failed");
|
|
1414
|
+
const tree = snapshotArtifactTree(pathname);
|
|
1415
|
+
return { path: pathname, identity, tree };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean {
|
|
1419
|
+
if (record.retainedArtifactsRoot === undefined) return true;
|
|
1420
|
+
if (!record.retainedArtifactsRoot || typeof record.retainedArtifactsRoot !== "object")
|
|
1421
|
+
throw new Error("durability_failed");
|
|
1422
|
+
const retained = record.retainedArtifactsRoot as Record<string, unknown>;
|
|
1423
|
+
const identity = retained.identity;
|
|
1424
|
+
const tree = artifactTreeSnapshot(retained.tree);
|
|
1425
|
+
if (
|
|
1426
|
+
typeof record.plannedArtifactsPath !== "string" ||
|
|
1427
|
+
retained.path !== deterministicRemovalRoot(record.plannedArtifactsPath) ||
|
|
1428
|
+
!identity ||
|
|
1429
|
+
typeof identity !== "object" ||
|
|
1430
|
+
Array.isArray(identity) ||
|
|
1431
|
+
!tree
|
|
1432
|
+
)
|
|
1433
|
+
throw new Error("durability_failed");
|
|
1434
|
+
const artifactIdentity = identity as Record<string, unknown>;
|
|
1435
|
+
if (
|
|
1436
|
+
typeof artifactIdentity.dev !== "string" ||
|
|
1437
|
+
typeof artifactIdentity.ino !== "string" ||
|
|
1438
|
+
typeof artifactIdentity.size !== "number" ||
|
|
1439
|
+
typeof artifactIdentity.mtimeNs !== "string" ||
|
|
1440
|
+
typeof artifactIdentity.sha256 !== "string"
|
|
1441
|
+
)
|
|
1442
|
+
throw new Error("durability_failed");
|
|
1443
|
+
if (!fs.existsSync(retained.path)) return true;
|
|
1444
|
+
const observed = artifactIdentityAt(retained.path);
|
|
1445
|
+
if (
|
|
1446
|
+
!observed ||
|
|
1447
|
+
!sameArtifactRootIdentity(observed, {
|
|
1448
|
+
dev: BigInt(artifactIdentity.dev),
|
|
1449
|
+
ino: BigInt(artifactIdentity.ino),
|
|
1450
|
+
size: artifactIdentity.size,
|
|
1451
|
+
mtimeNs: BigInt(artifactIdentity.mtimeNs),
|
|
1452
|
+
sha256: artifactIdentity.sha256,
|
|
1453
|
+
}) ||
|
|
1454
|
+
JSON.stringify(snapshotArtifactTree(retained.path)) !== JSON.stringify(tree)
|
|
1455
|
+
)
|
|
1456
|
+
throw new Error("durability_failed");
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1037
1460
|
type CleanupReceipt = {
|
|
1038
1461
|
attempt: number;
|
|
1039
1462
|
target: RetiredTarget;
|
|
@@ -1041,6 +1464,13 @@ type CleanupReceipt = {
|
|
|
1041
1464
|
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
1042
1465
|
detachedArtifactsPath?: string;
|
|
1043
1466
|
detachedTranscriptPath?: string;
|
|
1467
|
+
retainedArtifactsSuccessorPath?: string;
|
|
1468
|
+
retainedArtifactsPlaceholderPath?: string;
|
|
1469
|
+
retainedArtifactsUnknownPath?: string;
|
|
1470
|
+
retainedTranscriptSuccessorPath?: string;
|
|
1471
|
+
retainedTranscriptPlaceholderPath?: string;
|
|
1472
|
+
retainedTranscriptUnknownPath?: string;
|
|
1473
|
+
|
|
1044
1474
|
plannedArtifactsPath: string;
|
|
1045
1475
|
plannedTranscriptPath: string;
|
|
1046
1476
|
};
|
|
@@ -1074,6 +1504,24 @@ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: Cleanup
|
|
|
1074
1504
|
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
1075
1505
|
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
1076
1506
|
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
1507
|
+
...(receipt.retainedArtifactsSuccessorPath
|
|
1508
|
+
? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
|
|
1509
|
+
: {}),
|
|
1510
|
+
...(receipt.retainedArtifactsPlaceholderPath
|
|
1511
|
+
? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
|
|
1512
|
+
: {}),
|
|
1513
|
+
...(receipt.retainedArtifactsUnknownPath
|
|
1514
|
+
? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
|
|
1515
|
+
: {}),
|
|
1516
|
+
...(receipt.retainedTranscriptSuccessorPath
|
|
1517
|
+
? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
|
|
1518
|
+
: {}),
|
|
1519
|
+
...(receipt.retainedTranscriptPlaceholderPath
|
|
1520
|
+
? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
|
|
1521
|
+
: {}),
|
|
1522
|
+
...(receipt.retainedTranscriptUnknownPath
|
|
1523
|
+
? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
|
|
1524
|
+
: {}),
|
|
1077
1525
|
plannedArtifactsPath: receipt.plannedArtifactsPath,
|
|
1078
1526
|
plannedTranscriptPath: receipt.plannedTranscriptPath,
|
|
1079
1527
|
};
|
|
@@ -1094,6 +1542,7 @@ function cleanupArtifactsRemoved(
|
|
|
1094
1542
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1095
1543
|
const record = value as Record<string, unknown>;
|
|
1096
1544
|
const recorded = record.target as Record<string, unknown> | undefined;
|
|
1545
|
+
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
1097
1546
|
return (
|
|
1098
1547
|
record.schemaVersion === 2 &&
|
|
1099
1548
|
record.state === "artifacts_removed" &&
|
|
@@ -1102,9 +1551,17 @@ function cleanupArtifactsRemoved(
|
|
|
1102
1551
|
record.attempt === attempt &&
|
|
1103
1552
|
recorded?.path === target.path &&
|
|
1104
1553
|
recorded.sessionId === target.sessionId &&
|
|
1105
|
-
recorded.cwd === target.cwd
|
|
1554
|
+
recorded.cwd === target.cwd &&
|
|
1555
|
+
identity?.canonicalPath === target.identity.canonicalPath &&
|
|
1556
|
+
identity.dev === String(target.identity.dev) &&
|
|
1557
|
+
identity.ino === String(target.identity.ino) &&
|
|
1558
|
+
identity.size === target.identity.size &&
|
|
1559
|
+
identity.mtimeNs === String(target.identity.mtimeNs) &&
|
|
1560
|
+
identity.sha256 === target.identity.sha256 &&
|
|
1561
|
+
retainedArtifactsRootMatches(record)
|
|
1106
1562
|
);
|
|
1107
|
-
} catch {
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
if ((error as Error).message === "durability_failed") throw error;
|
|
1108
1565
|
return false;
|
|
1109
1566
|
}
|
|
1110
1567
|
}
|
|
@@ -1115,9 +1572,14 @@ async function publishCleanupArtifactsRemoved(
|
|
|
1115
1572
|
receipt: CleanupReceipt,
|
|
1116
1573
|
lock: ManagedStorageLock,
|
|
1117
1574
|
): Promise<void> {
|
|
1575
|
+
const retainedArtifactsRoot = retainedArtifactsRootReceipt(receipt);
|
|
1118
1576
|
await publishManagedTombstone(
|
|
1119
1577
|
cleanupReceiptPath(tombstone, receipt.target, "artifacts_removed", receipt.attempt),
|
|
1120
|
-
{
|
|
1578
|
+
{
|
|
1579
|
+
...cleanupReceipt(scope, tombstone, receipt),
|
|
1580
|
+
state: "artifacts_removed",
|
|
1581
|
+
...(retainedArtifactsRoot ? { retainedArtifactsRoot } : {}),
|
|
1582
|
+
},
|
|
1121
1583
|
lock.assertOwned,
|
|
1122
1584
|
).catch(error => {
|
|
1123
1585
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
@@ -1134,6 +1596,14 @@ function isQuarantinePath(target: RetiredTarget, pathname: unknown): pathname is
|
|
|
1134
1596
|
);
|
|
1135
1597
|
}
|
|
1136
1598
|
|
|
1599
|
+
function isRetainedNativePath(target: RetiredTarget, pathname: unknown): pathname is string {
|
|
1600
|
+
return (
|
|
1601
|
+
typeof pathname === "string" &&
|
|
1602
|
+
path.dirname(pathname) === path.dirname(target.path) &&
|
|
1603
|
+
path.basename(pathname).startsWith(".skc-")
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1137
1607
|
function deterministicRemovalRoot(plannedRoot: string): string {
|
|
1138
1608
|
return `${plannedRoot}.removing`;
|
|
1139
1609
|
}
|
|
@@ -1145,17 +1615,44 @@ function isAuthorizedArtifactRoot(target: RetiredTarget, plannedRoot: string, pa
|
|
|
1145
1615
|
);
|
|
1146
1616
|
}
|
|
1147
1617
|
|
|
1148
|
-
function
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1618
|
+
function transcriptRootMatchesTarget(pathname: string, target: RetiredTarget): boolean {
|
|
1619
|
+
try {
|
|
1620
|
+
const observed = captureManagedFileNoFollow(pathname);
|
|
1621
|
+
const digest = createHash("sha256").update(observed.bytes).digest("hex");
|
|
1622
|
+
return (
|
|
1623
|
+
observed.identity.dev === target.identity.dev &&
|
|
1624
|
+
observed.identity.ino === target.identity.ino &&
|
|
1625
|
+
observed.identity.size === target.identity.size &&
|
|
1626
|
+
observed.identity.mtimeNs === target.identity.mtimeNs &&
|
|
1627
|
+
digest === target.identity.sha256
|
|
1628
|
+
);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
1631
|
+
throw new Error("durability_failed");
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function cleanupRootsAbsent(tombstone: string, target: RetiredTarget, pending: CleanupReceipt): boolean {
|
|
1154
1636
|
const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
|
|
1637
|
+
const activeTranscriptRoots = new Set(
|
|
1638
|
+
[pending.plannedTranscriptPath, pending.detachedTranscriptPath].filter((pathname): pathname is string =>
|
|
1639
|
+
isQuarantinePath(target, pathname),
|
|
1640
|
+
),
|
|
1641
|
+
);
|
|
1155
1642
|
const roots = new Set<string>([
|
|
1643
|
+
target.path,
|
|
1644
|
+
target.path.slice(0, -6),
|
|
1156
1645
|
pending.plannedArtifactsPath,
|
|
1157
1646
|
deterministicRemovalRoot(pending.plannedArtifactsPath),
|
|
1158
1647
|
...(pending.detachedArtifactsPath ? [pending.detachedArtifactsPath] : []),
|
|
1648
|
+
...[
|
|
1649
|
+
pending.retainedArtifactsSuccessorPath,
|
|
1650
|
+
pending.retainedArtifactsPlaceholderPath,
|
|
1651
|
+
pending.retainedArtifactsUnknownPath,
|
|
1652
|
+
pending.retainedTranscriptSuccessorPath,
|
|
1653
|
+
pending.retainedTranscriptPlaceholderPath,
|
|
1654
|
+
pending.retainedTranscriptUnknownPath,
|
|
1655
|
+
].filter((pathname): pathname is string => isRetainedNativePath(target, pathname)),
|
|
1159
1656
|
]);
|
|
1160
1657
|
for (const name of fs.readdirSync(path.dirname(tombstone))) {
|
|
1161
1658
|
if (!name.startsWith(prefix) || !name.endsWith(".json")) continue;
|
|
@@ -1164,26 +1661,91 @@ function artifactRootsAbsent(
|
|
|
1164
1661
|
) as {
|
|
1165
1662
|
plannedArtifactsPath?: unknown;
|
|
1166
1663
|
detachedArtifactsPath?: unknown;
|
|
1664
|
+
plannedTranscriptPath?: unknown;
|
|
1665
|
+
detachedTranscriptPath?: unknown;
|
|
1666
|
+
retainedArtifactsSuccessorPath?: unknown;
|
|
1667
|
+
retainedArtifactsPlaceholderPath?: unknown;
|
|
1668
|
+
retainedArtifactsUnknownPath?: unknown;
|
|
1669
|
+
retainedTranscriptSuccessorPath?: unknown;
|
|
1670
|
+
retainedTranscriptPlaceholderPath?: unknown;
|
|
1671
|
+
retainedTranscriptUnknownPath?: unknown;
|
|
1167
1672
|
};
|
|
1168
1673
|
if (isQuarantinePath(target, record.plannedArtifactsPath)) {
|
|
1169
1674
|
roots.add(record.plannedArtifactsPath);
|
|
1170
1675
|
roots.add(deterministicRemovalRoot(record.plannedArtifactsPath));
|
|
1171
1676
|
}
|
|
1172
1677
|
if (isQuarantinePath(target, record.detachedArtifactsPath)) roots.add(record.detachedArtifactsPath);
|
|
1678
|
+
for (const pathname of [record.plannedTranscriptPath, record.detachedTranscriptPath]) {
|
|
1679
|
+
if (isQuarantinePath(target, pathname) && !activeTranscriptRoots.has(pathname)) roots.add(pathname);
|
|
1680
|
+
}
|
|
1681
|
+
for (const pathname of [
|
|
1682
|
+
record.retainedArtifactsSuccessorPath,
|
|
1683
|
+
record.retainedArtifactsPlaceholderPath,
|
|
1684
|
+
record.retainedArtifactsUnknownPath,
|
|
1685
|
+
record.retainedTranscriptSuccessorPath,
|
|
1686
|
+
record.retainedTranscriptPlaceholderPath,
|
|
1687
|
+
record.retainedTranscriptUnknownPath,
|
|
1688
|
+
]) {
|
|
1689
|
+
if (isRetainedNativePath(target, pathname)) roots.add(pathname);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
for (const pathname of roots) {
|
|
1693
|
+
try {
|
|
1694
|
+
fs.lstatSync(pathname);
|
|
1695
|
+
throw new Error("durability_failed");
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1698
|
+
throw error;
|
|
1699
|
+
}
|
|
1173
1700
|
}
|
|
1174
|
-
|
|
1701
|
+
for (const pathname of activeTranscriptRoots) {
|
|
1702
|
+
try {
|
|
1703
|
+
fs.lstatSync(pathname);
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1706
|
+
throw error;
|
|
1707
|
+
}
|
|
1708
|
+
if (!transcriptRootMatchesTarget(pathname, target)) throw new Error("durability_failed");
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
return true;
|
|
1175
1712
|
}
|
|
1176
1713
|
|
|
1177
1714
|
function sameArtifactRootIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
|
|
1178
1715
|
return left.dev === right.dev && left.ino === right.ino;
|
|
1179
1716
|
}
|
|
1180
1717
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1718
|
+
function sameArtifactTreeSnapshot(left: NativeDirectoryTreeSnapshot, right: NativeDirectoryTreeSnapshot): boolean {
|
|
1719
|
+
if (left.rootDev !== right.rootDev || left.rootIno !== right.rootIno || left.entries.length !== right.entries.length)
|
|
1720
|
+
return false;
|
|
1721
|
+
const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string =>
|
|
1722
|
+
JSON.stringify([entry.relativePath, entry.kind, entry.dev, entry.ino, entry.size, entry.mtimeNs, entry.sha256]);
|
|
1723
|
+
const leftEntries = left.entries.map(entryKey).sort();
|
|
1724
|
+
const rightEntries = right.entries.map(entryKey).sort();
|
|
1725
|
+
return leftEntries.every((entry, index) => entry === rightEntries[index]);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
1729
|
+
if (!pending.detachedArtifactsPath) return;
|
|
1730
|
+
if (!pending.expectedArtifactsIdentity || !pending.expectedArtifactsTree) throw new Error("durability_failed");
|
|
1731
|
+
try {
|
|
1732
|
+
fs.lstatSync(pending.detachedArtifactsPath);
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
1735
|
+
throw new Error("durability_failed");
|
|
1736
|
+
}
|
|
1737
|
+
const observed = artifactIdentityAt(pending.detachedArtifactsPath);
|
|
1738
|
+
if (
|
|
1739
|
+
!observed ||
|
|
1740
|
+
!sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
|
|
1741
|
+
!sameArtifactTreeSnapshot(snapshotArtifactTree(pending.detachedArtifactsPath), pending.expectedArtifactsTree)
|
|
1742
|
+
)
|
|
1743
|
+
throw new Error("binding_invalid");
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1186
1746
|
function probePlannedCleanupDetach(target: RetiredTarget, pending: CleanupReceipt): CleanupReceipt {
|
|
1747
|
+
assertRetainedArtifactsAuthority(pending);
|
|
1748
|
+
|
|
1187
1749
|
let detachedArtifactsPath = pending.detachedArtifactsPath;
|
|
1188
1750
|
let detachedTranscriptPath = pending.detachedTranscriptPath;
|
|
1189
1751
|
for (const pathname of [pending.plannedArtifactsPath, deterministicRemovalRoot(pending.plannedArtifactsPath)]) {
|
|
@@ -1247,6 +1809,16 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
1247
1809
|
})
|
|
1248
1810
|
)
|
|
1249
1811
|
return undefined;
|
|
1812
|
+
const roots = snapshot.entries.filter(entry => {
|
|
1813
|
+
const item = entry as Record<string, unknown>;
|
|
1814
|
+
return (
|
|
1815
|
+
item.relativePath === "" &&
|
|
1816
|
+
item.kind === "directory" &&
|
|
1817
|
+
item.dev === snapshot.rootDev &&
|
|
1818
|
+
item.ino === snapshot.rootIno
|
|
1819
|
+
);
|
|
1820
|
+
});
|
|
1821
|
+
if (roots.length !== 1) return undefined;
|
|
1250
1822
|
return snapshot as unknown as NativeDirectoryTreeSnapshot;
|
|
1251
1823
|
}
|
|
1252
1824
|
|
|
@@ -1298,7 +1870,19 @@ function pendingCleanupReceipt(
|
|
|
1298
1870
|
plannedPaths.has(record.plannedArtifactsPath as string) ||
|
|
1299
1871
|
plannedPaths.has(record.plannedTranscriptPath as string) ||
|
|
1300
1872
|
(record.detachedArtifactsPath !== undefined && !isQuarantinePath(target, record.detachedArtifactsPath)) ||
|
|
1301
|
-
(record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath))
|
|
1873
|
+
(record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath)) ||
|
|
1874
|
+
(record.retainedArtifactsSuccessorPath !== undefined &&
|
|
1875
|
+
!isRetainedNativePath(target, record.retainedArtifactsSuccessorPath)) ||
|
|
1876
|
+
(record.retainedArtifactsPlaceholderPath !== undefined &&
|
|
1877
|
+
!isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) ||
|
|
1878
|
+
(record.retainedArtifactsUnknownPath !== undefined &&
|
|
1879
|
+
!isRetainedNativePath(target, record.retainedArtifactsUnknownPath)) ||
|
|
1880
|
+
(record.retainedTranscriptSuccessorPath !== undefined &&
|
|
1881
|
+
!isRetainedNativePath(target, record.retainedTranscriptSuccessorPath)) ||
|
|
1882
|
+
(record.retainedTranscriptPlaceholderPath !== undefined &&
|
|
1883
|
+
!isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
|
|
1884
|
+
(record.retainedTranscriptUnknownPath !== undefined &&
|
|
1885
|
+
!isRetainedNativePath(target, record.retainedTranscriptUnknownPath))
|
|
1302
1886
|
)
|
|
1303
1887
|
throw new Error("durability_failed");
|
|
1304
1888
|
const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
|
|
@@ -1321,7 +1905,7 @@ function pendingCleanupReceipt(
|
|
|
1321
1905
|
const expectedArtifactsTree =
|
|
1322
1906
|
record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
|
|
1323
1907
|
if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
|
|
1324
|
-
|
|
1908
|
+
|
|
1325
1909
|
if (
|
|
1326
1910
|
latest &&
|
|
1327
1911
|
((record.detachedArtifactsPath !== undefined &&
|
|
@@ -1340,6 +1924,12 @@ function pendingCleanupReceipt(
|
|
|
1340
1924
|
expectedArtifactsTree,
|
|
1341
1925
|
detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
|
|
1342
1926
|
detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
|
|
1927
|
+
retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
|
|
1928
|
+
retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
|
|
1929
|
+
retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
|
|
1930
|
+
retainedTranscriptSuccessorPath: record.retainedTranscriptSuccessorPath as string | undefined,
|
|
1931
|
+
retainedTranscriptPlaceholderPath: record.retainedTranscriptPlaceholderPath as string | undefined,
|
|
1932
|
+
retainedTranscriptUnknownPath: record.retainedTranscriptUnknownPath as string | undefined,
|
|
1343
1933
|
plannedArtifactsPath: record.plannedArtifactsPath as string,
|
|
1344
1934
|
plannedTranscriptPath: record.plannedTranscriptPath as string,
|
|
1345
1935
|
};
|
|
@@ -1389,7 +1979,13 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
|
|
|
1389
1979
|
expectedArtifactsIdentity,
|
|
1390
1980
|
expectedArtifactsTree,
|
|
1391
1981
|
detachedArtifactsPath: pending?.detachedArtifactsPath,
|
|
1982
|
+
retainedArtifactsSuccessorPath: pending?.retainedArtifactsSuccessorPath,
|
|
1983
|
+
retainedArtifactsPlaceholderPath: pending?.retainedArtifactsPlaceholderPath,
|
|
1984
|
+
retainedArtifactsUnknownPath: pending?.retainedArtifactsUnknownPath,
|
|
1392
1985
|
detachedTranscriptPath: pending?.detachedTranscriptPath,
|
|
1986
|
+
retainedTranscriptSuccessorPath: pending?.retainedTranscriptSuccessorPath,
|
|
1987
|
+
retainedTranscriptPlaceholderPath: pending?.retainedTranscriptPlaceholderPath,
|
|
1988
|
+
retainedTranscriptUnknownPath: pending?.retainedTranscriptUnknownPath,
|
|
1393
1989
|
plannedArtifactsPath: path.join(directory, `.skc-delete-${operation}-artifacts-${attempt}`),
|
|
1394
1990
|
plannedTranscriptPath: path.join(directory, `.skc-delete-${operation}-transcript-${attempt}`),
|
|
1395
1991
|
};
|
|
@@ -1402,6 +1998,36 @@ function requiresFreshCleanupPlan(pending: CleanupReceipt): boolean {
|
|
|
1402
1998
|
);
|
|
1403
1999
|
}
|
|
1404
2000
|
|
|
2001
|
+
/** Evidence-carrier for a verified `cleanup_pending` deletion, bound to the active plan. */
|
|
2002
|
+
function cleanupPendingEvidence(
|
|
2003
|
+
retry: CleanupReceipt,
|
|
2004
|
+
active: CleanupReceipt,
|
|
2005
|
+
deletion: Extract<VerifiedSessionDeleteResult, { kind: "cleanup_pending" }>,
|
|
2006
|
+
): CleanupReceipt {
|
|
2007
|
+
return {
|
|
2008
|
+
...retry,
|
|
2009
|
+
expectedArtifactsIdentity:
|
|
2010
|
+
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
2011
|
+
expectedArtifactsTree: deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2012
|
+
detachedArtifactsPath:
|
|
2013
|
+
deletion.phase === "artifacts" ? deletion.detachedArtifactsPath : active.detachedArtifactsPath,
|
|
2014
|
+
detachedTranscriptPath:
|
|
2015
|
+
deletion.phase === "transcript" ? deletion.detachedTranscriptPath : active.detachedTranscriptPath,
|
|
2016
|
+
retainedArtifactsSuccessorPath:
|
|
2017
|
+
deletion.phase === "artifacts" ? deletion.retainedSuccessorPath : active.retainedArtifactsSuccessorPath,
|
|
2018
|
+
retainedArtifactsPlaceholderPath:
|
|
2019
|
+
deletion.phase === "artifacts" ? deletion.retainedPlaceholderPath : active.retainedArtifactsPlaceholderPath,
|
|
2020
|
+
retainedArtifactsUnknownPath:
|
|
2021
|
+
deletion.phase === "artifacts" ? deletion.retainedUnknownPath : active.retainedArtifactsUnknownPath,
|
|
2022
|
+
retainedTranscriptSuccessorPath:
|
|
2023
|
+
deletion.phase === "transcript" ? deletion.retainedSuccessorPath : active.retainedTranscriptSuccessorPath,
|
|
2024
|
+
retainedTranscriptPlaceholderPath:
|
|
2025
|
+
deletion.phase === "transcript" ? deletion.retainedPlaceholderPath : active.retainedTranscriptPlaceholderPath,
|
|
2026
|
+
retainedTranscriptUnknownPath:
|
|
2027
|
+
deletion.phase === "transcript" ? deletion.retainedUnknownPath : active.retainedTranscriptUnknownPath,
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1405
2031
|
async function publishCleanupPending(
|
|
1406
2032
|
scope: ManagedScope,
|
|
1407
2033
|
tombstone: string,
|
|
@@ -1653,11 +2279,19 @@ function planArtifactRootForMigration(sourceTranscript: string, operation: strin
|
|
|
1653
2279
|
throw error;
|
|
1654
2280
|
}
|
|
1655
2281
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe_artifacts");
|
|
2282
|
+
const tree = snapshotArtifactTree(originalPath);
|
|
2283
|
+
const root = tree.entries.find(entry => entry.relativePath === "" && entry.kind === "directory");
|
|
2284
|
+
if (!root) throw new Error("unsafe_artifacts");
|
|
1656
2285
|
return {
|
|
1657
2286
|
originalPath,
|
|
1658
2287
|
detachedPath: path.join(path.dirname(originalPath), `.skc-migrate-${operation}-artifacts`),
|
|
1659
|
-
identity: {
|
|
1660
|
-
|
|
2288
|
+
identity: {
|
|
2289
|
+
dev: stat.dev,
|
|
2290
|
+
ino: stat.ino,
|
|
2291
|
+
size: process.platform === "win32" ? BigInt(root.size) : stat.size,
|
|
2292
|
+
mtimeNs: process.platform === "win32" ? BigInt(root.mtimeNs) : stat.mtimeNs,
|
|
2293
|
+
},
|
|
2294
|
+
tree,
|
|
1661
2295
|
};
|
|
1662
2296
|
}
|
|
1663
2297
|
|
|
@@ -1678,44 +2312,223 @@ function sameDirectoryObject(leftPath: string, rightPath: string): boolean {
|
|
|
1678
2312
|
}
|
|
1679
2313
|
}
|
|
1680
2314
|
|
|
1681
|
-
function
|
|
2315
|
+
export function matchesMigrationArtifactRoot(
|
|
2316
|
+
pathname: string,
|
|
2317
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint },
|
|
2318
|
+
expectedTree: NativeDirectoryTreeSnapshot,
|
|
2319
|
+
platform: NodeJS.Platform = process.platform,
|
|
2320
|
+
): boolean {
|
|
1682
2321
|
try {
|
|
1683
2322
|
const stat = fs.lstatSync(pathname, { bigint: true });
|
|
2323
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== identity.dev || stat.ino !== identity.ino)
|
|
2324
|
+
return false;
|
|
2325
|
+
const observed = native.snapshotDirectoryTree(pathname);
|
|
2326
|
+
const expectedRoot = expectedTree.entries.find(entry => entry.relativePath === "" && entry.kind === "directory");
|
|
2327
|
+
const observedRoot = observed.snapshot?.entries.find(
|
|
2328
|
+
entry => entry.relativePath === "" && entry.kind === "directory",
|
|
2329
|
+
);
|
|
2330
|
+
if (
|
|
2331
|
+
!observed.ok ||
|
|
2332
|
+
!observed.snapshot ||
|
|
2333
|
+
!expectedRoot ||
|
|
2334
|
+
!observedRoot ||
|
|
2335
|
+
(platform === "win32" &&
|
|
2336
|
+
(observedRoot.dev !== identity.dev.toString() ||
|
|
2337
|
+
observedRoot.ino !== identity.ino.toString() ||
|
|
2338
|
+
BigInt(observedRoot.size) !== identity.size ||
|
|
2339
|
+
BigInt(observedRoot.mtimeNs) !== identity.mtimeNs)) ||
|
|
2340
|
+
(platform !== "win32" && (stat.size !== identity.size || stat.mtimeNs !== identity.mtimeNs))
|
|
2341
|
+
)
|
|
2342
|
+
return false;
|
|
2343
|
+
return sameArtifactTree(expectedTree, observed.snapshot, { platform, phase: "migration" });
|
|
2344
|
+
} catch {
|
|
2345
|
+
return false;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
type ArtifactTreeComparisonPolicy = {
|
|
2350
|
+
platform?: NodeJS.Platform;
|
|
2351
|
+
phase?: "migration" | "strict";
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
function sameArtifactTree(
|
|
2355
|
+
left: NativeDirectoryTreeSnapshot,
|
|
2356
|
+
right: NativeDirectoryTreeSnapshot,
|
|
2357
|
+
policy: ArtifactTreeComparisonPolicy = {},
|
|
2358
|
+
): boolean {
|
|
2359
|
+
const { platform = process.platform, phase = "strict" } = policy;
|
|
2360
|
+
const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string => {
|
|
2361
|
+
// Detaching the root directory changes its ctime without changing the preserved artifact tree;
|
|
2362
|
+
// root identity and mtime are checked above. pi-iso also accepts Windows plain directories by
|
|
2363
|
+
// stable file id and kind only because NTFS can lazily update their metadata while enumeration
|
|
2364
|
+
// trails an open handle. All other platforms and nested directories compare every captured field.
|
|
2365
|
+
if (phase === "migration" && entry.kind === "directory" && (entry.relativePath === "" || platform === "win32"))
|
|
2366
|
+
return JSON.stringify([entry.relativePath, entry.kind, entry.dev, entry.ino]);
|
|
2367
|
+
return JSON.stringify([
|
|
2368
|
+
entry.relativePath,
|
|
2369
|
+
entry.kind,
|
|
2370
|
+
entry.dev,
|
|
2371
|
+
entry.ino,
|
|
2372
|
+
entry.size,
|
|
2373
|
+
entry.mtimeNs,
|
|
2374
|
+
entry.ctimeNs,
|
|
2375
|
+
entry.sha256,
|
|
2376
|
+
]);
|
|
2377
|
+
};
|
|
2378
|
+
const leftEntries = left.entries.map(entryKey).sort();
|
|
2379
|
+
const rightEntries = right.entries.map(entryKey).sort();
|
|
2380
|
+
return (
|
|
2381
|
+
left.rootDev === right.rootDev &&
|
|
2382
|
+
left.rootIno === right.rootIno &&
|
|
2383
|
+
leftEntries.length === rightEntries.length &&
|
|
2384
|
+
leftEntries.every((entry, index) => entry === rightEntries[index])
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function matchesDetachedArtifactRoot(pathname: string, plan: DetachedArtifactRoot): boolean {
|
|
2389
|
+
return (
|
|
2390
|
+
matchesMigrationArtifactRoot(pathname, plan.identity, plan.tree) &&
|
|
2391
|
+
sameDirectoryObject(path.dirname(pathname), path.dirname(plan.detachedPath))
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
type SourceArtifactCleanup = {
|
|
2396
|
+
state: "cleanup_pending";
|
|
2397
|
+
role: "exchange_placeholder";
|
|
2398
|
+
retainedPath: string;
|
|
2399
|
+
identity: DetachedArtifactRoot["identity"];
|
|
2400
|
+
tree: NativeDirectoryTreeSnapshot;
|
|
2401
|
+
};
|
|
2402
|
+
|
|
2403
|
+
export function cleanupAuthorityMatches(
|
|
2404
|
+
cleanup: SourceArtifactCleanup,
|
|
2405
|
+
parent: string,
|
|
2406
|
+
platform: NodeJS.Platform = process.platform,
|
|
2407
|
+
): boolean {
|
|
2408
|
+
try {
|
|
2409
|
+
const stat = fs.lstatSync(cleanup.retainedPath, { bigint: true });
|
|
2410
|
+
if (
|
|
2411
|
+
path.dirname(cleanup.retainedPath) !== parent ||
|
|
2412
|
+
!stat.isDirectory() ||
|
|
2413
|
+
stat.isSymbolicLink() ||
|
|
2414
|
+
stat.dev !== cleanup.identity.dev ||
|
|
2415
|
+
stat.ino !== cleanup.identity.ino
|
|
2416
|
+
)
|
|
2417
|
+
return false;
|
|
2418
|
+
const snapshot = native.snapshotDirectoryTree(cleanup.retainedPath);
|
|
2419
|
+
const observedRoot = snapshot.snapshot?.entries.find(
|
|
2420
|
+
entry => entry.relativePath === "" && entry.kind === "directory",
|
|
2421
|
+
);
|
|
2422
|
+
if (
|
|
2423
|
+
!snapshot.ok ||
|
|
2424
|
+
!snapshot.snapshot ||
|
|
2425
|
+
!observedRoot ||
|
|
2426
|
+
(platform === "win32"
|
|
2427
|
+
? observedRoot.dev !== cleanup.identity.dev.toString() ||
|
|
2428
|
+
observedRoot.ino !== cleanup.identity.ino.toString() ||
|
|
2429
|
+
BigInt(observedRoot.size) !== cleanup.identity.size ||
|
|
2430
|
+
BigInt(observedRoot.mtimeNs) !== cleanup.identity.mtimeNs
|
|
2431
|
+
: stat.size !== cleanup.identity.size || stat.mtimeNs !== cleanup.identity.mtimeNs)
|
|
2432
|
+
)
|
|
2433
|
+
return false;
|
|
1684
2434
|
return (
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
stat.size === plan.identity.size &&
|
|
1690
|
-
stat.mtimeNs === plan.identity.mtimeNs &&
|
|
1691
|
-
sameDirectoryObject(path.dirname(pathname), path.dirname(plan.detachedPath))
|
|
2435
|
+
sameArtifactTree(snapshot.snapshot, cleanup.tree) &&
|
|
2436
|
+
cleanup.tree.entries.length === 1 &&
|
|
2437
|
+
cleanup.tree.entries[0]?.relativePath === "" &&
|
|
2438
|
+
cleanup.tree.entries[0]?.kind === "directory"
|
|
1692
2439
|
);
|
|
1693
2440
|
} catch {
|
|
1694
2441
|
return false;
|
|
1695
2442
|
}
|
|
1696
2443
|
}
|
|
1697
2444
|
|
|
1698
|
-
function detachArtifactRootForMigration(
|
|
2445
|
+
export function detachArtifactRootForMigration(
|
|
2446
|
+
plan: DetachedArtifactRoot,
|
|
2447
|
+
platform: NodeJS.Platform = process.platform,
|
|
2448
|
+
):
|
|
2449
|
+
| { detached: DetachedArtifactRoot; detachOutcome: "clean" }
|
|
2450
|
+
| { detached: DetachedArtifactRoot; detachOutcome: "cleanup_pending"; cleanup: SourceArtifactCleanup } {
|
|
1699
2451
|
const result = native.exactUnlink(plan.originalPath, {
|
|
1700
2452
|
...plan.identity,
|
|
1701
2453
|
directory: true,
|
|
1702
2454
|
detachOnly: true,
|
|
1703
2455
|
quarantineName: path.basename(plan.detachedPath),
|
|
1704
2456
|
});
|
|
1705
|
-
|
|
2457
|
+
const cleanSuccess =
|
|
2458
|
+
result.ok && !result.retainedSuccessorPath && !result.retainedPlaceholderPath && !result.retainedUnknownPath;
|
|
2459
|
+
const cleanupPending =
|
|
2460
|
+
!result.ok &&
|
|
2461
|
+
result.code === "cleanup_pending" &&
|
|
2462
|
+
!!result.detachedPath &&
|
|
2463
|
+
!!result.retainedPlaceholderPath &&
|
|
2464
|
+
!result.retainedSuccessorPath &&
|
|
2465
|
+
!result.retainedUnknownPath;
|
|
2466
|
+
if (
|
|
2467
|
+
(!cleanSuccess && !cleanupPending) ||
|
|
2468
|
+
!result.detachedPath ||
|
|
2469
|
+
!matchesDetachedArtifactRoot(result.detachedPath, plan)
|
|
2470
|
+
)
|
|
2471
|
+
throw new Error("durability_failed");
|
|
2472
|
+
|
|
2473
|
+
const detachedPath =
|
|
2474
|
+
process.platform === "win32" ? fs.realpathSync.native(result.detachedPath) : result.detachedPath;
|
|
2475
|
+
if (!matchesDetachedArtifactRoot(detachedPath, plan)) throw new Error("durability_failed");
|
|
2476
|
+
const detached = { ...plan, detachedPath };
|
|
2477
|
+
if (!cleanupPending) return { detached, detachOutcome: "clean" };
|
|
2478
|
+
const placeholder = result.retainedPlaceholderPath!;
|
|
2479
|
+
const stat = fs.lstatSync(placeholder, { bigint: true });
|
|
2480
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || path.dirname(placeholder) !== path.dirname(plan.originalPath))
|
|
2481
|
+
throw new Error("durability_failed");
|
|
2482
|
+
const snapshot = native.snapshotDirectoryTree(placeholder);
|
|
2483
|
+
if (!snapshot.ok || !snapshot.snapshot) throw new Error("durability_failed");
|
|
2484
|
+
// Windows directory size/mtime authority is the native tree root, never Bun's
|
|
2485
|
+
// zero-valued directory lstat. Capturing Bun values here would guarantee a
|
|
2486
|
+
// mismatch against the native-authoritative check below.
|
|
2487
|
+
const placeholderRoot = snapshot.snapshot.entries.find(
|
|
2488
|
+
entry => entry.relativePath === "" && entry.kind === "directory",
|
|
2489
|
+
);
|
|
2490
|
+
if (!placeholderRoot) throw new Error("durability_failed");
|
|
2491
|
+
const cleanup: SourceArtifactCleanup = {
|
|
2492
|
+
state: "cleanup_pending",
|
|
2493
|
+
role: "exchange_placeholder",
|
|
2494
|
+
retainedPath: placeholder,
|
|
2495
|
+
identity: {
|
|
2496
|
+
dev: stat.dev,
|
|
2497
|
+
ino: stat.ino,
|
|
2498
|
+
size: platform === "win32" ? BigInt(placeholderRoot.size) : stat.size,
|
|
2499
|
+
mtimeNs: platform === "win32" ? BigInt(placeholderRoot.mtimeNs) : stat.mtimeNs,
|
|
2500
|
+
},
|
|
2501
|
+
tree: snapshot.snapshot,
|
|
2502
|
+
};
|
|
2503
|
+
if (!cleanupAuthorityMatches(cleanup, path.dirname(plan.originalPath), platform))
|
|
1706
2504
|
throw new Error("durability_failed");
|
|
1707
|
-
return {
|
|
2505
|
+
return { detached, detachOutcome: "cleanup_pending", cleanup };
|
|
1708
2506
|
}
|
|
1709
2507
|
|
|
1710
|
-
function restorePreparedArtifactRoot(
|
|
1711
|
-
|
|
2508
|
+
export function restorePreparedArtifactRoot(
|
|
2509
|
+
scope: ManagedScope,
|
|
2510
|
+
source: ManagedCandidate,
|
|
2511
|
+
lock?: ManagedStorageLock,
|
|
2512
|
+
): void {
|
|
2513
|
+
const preparedReceipt = receiptPathFor(scope, source, "prepared");
|
|
2514
|
+
const detachedReceipt = receiptPathFor(scope, source, "detached");
|
|
2515
|
+
const receipt = fs.existsSync(detachedReceipt) ? detachedReceipt : preparedReceipt;
|
|
1712
2516
|
let record: {
|
|
1713
2517
|
sourceArtifactQuarantine?: {
|
|
1714
2518
|
path?: unknown;
|
|
1715
2519
|
detachedPath?: unknown;
|
|
1716
2520
|
identity?: Record<string, unknown>;
|
|
1717
2521
|
tree?: unknown;
|
|
2522
|
+
role?: unknown;
|
|
1718
2523
|
};
|
|
2524
|
+
sourceArtifactCleanup?: {
|
|
2525
|
+
state?: unknown;
|
|
2526
|
+
role?: unknown;
|
|
2527
|
+
retainedPath?: unknown;
|
|
2528
|
+
identity?: Record<string, unknown>;
|
|
2529
|
+
tree?: unknown;
|
|
2530
|
+
};
|
|
2531
|
+
detachOutcome?: unknown;
|
|
1719
2532
|
};
|
|
1720
2533
|
try {
|
|
1721
2534
|
record = JSON.parse(captureManagedFileNoFollow(receipt).bytes.toString("utf8")) as typeof record;
|
|
@@ -1739,39 +2552,105 @@ function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandida
|
|
|
1739
2552
|
typeof identity.mtimeNs !== "string"
|
|
1740
2553
|
)
|
|
1741
2554
|
throw new Error("durability_failed");
|
|
2555
|
+
const artifactIdentity = {
|
|
2556
|
+
dev: BigInt(identity.dev),
|
|
2557
|
+
ino: BigInt(identity.ino),
|
|
2558
|
+
size: BigInt(identity.size),
|
|
2559
|
+
mtimeNs: BigInt(identity.mtimeNs),
|
|
2560
|
+
};
|
|
1742
2561
|
const expectedTree = artifactTreeSnapshot(quarantine.tree)!;
|
|
1743
2562
|
const assertPreparedTree = (pathname: string): void => {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2563
|
+
if (
|
|
2564
|
+
!matchesMigrationArtifactRoot(
|
|
2565
|
+
pathname,
|
|
2566
|
+
{
|
|
2567
|
+
...artifactIdentity,
|
|
2568
|
+
},
|
|
2569
|
+
expectedTree,
|
|
2570
|
+
)
|
|
2571
|
+
)
|
|
1747
2572
|
throw new Error("durability_failed");
|
|
1748
2573
|
};
|
|
2574
|
+
if (receipt === detachedReceipt) {
|
|
2575
|
+
if (quarantine.role !== "detached_artifact_root") throw new Error("durability_failed");
|
|
2576
|
+
if (record.detachOutcome === "clean") {
|
|
2577
|
+
if (record.sourceArtifactCleanup !== undefined) throw new Error("durability_failed");
|
|
2578
|
+
const originalExists = fs.existsSync(quarantine.path);
|
|
2579
|
+
const detachedExists = fs.existsSync(quarantine.detachedPath);
|
|
2580
|
+
if (originalExists) {
|
|
2581
|
+
if (detachedExists) throw new Error("durability_failed");
|
|
2582
|
+
assertPreparedTree(quarantine.path);
|
|
2583
|
+
if (lock) {
|
|
2584
|
+
lock.assertOwned();
|
|
2585
|
+
fs.unlinkSync(receipt);
|
|
2586
|
+
fsyncManagedParent(receipt);
|
|
2587
|
+
}
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
} else if (record.detachOutcome === "cleanup_pending") {
|
|
2591
|
+
const cleanup = record.sourceArtifactCleanup;
|
|
2592
|
+
if (!cleanup) throw new Error("durability_failed");
|
|
2593
|
+
const cleanupIdentity = cleanup.identity;
|
|
2594
|
+
const cleanupTree = artifactTreeSnapshot(cleanup.tree);
|
|
2595
|
+
if (
|
|
2596
|
+
cleanup.state !== "cleanup_pending" ||
|
|
2597
|
+
cleanup.role !== "exchange_placeholder" ||
|
|
2598
|
+
typeof cleanup.retainedPath !== "string" ||
|
|
2599
|
+
!cleanupIdentity ||
|
|
2600
|
+
typeof cleanupIdentity.dev !== "string" ||
|
|
2601
|
+
typeof cleanupIdentity.ino !== "string" ||
|
|
2602
|
+
typeof cleanupIdentity.size !== "string" ||
|
|
2603
|
+
typeof cleanupIdentity.mtimeNs !== "string" ||
|
|
2604
|
+
!cleanupTree ||
|
|
2605
|
+
!cleanupAuthorityMatches(
|
|
2606
|
+
{
|
|
2607
|
+
state: "cleanup_pending",
|
|
2608
|
+
role: "exchange_placeholder",
|
|
2609
|
+
retainedPath: cleanup.retainedPath,
|
|
2610
|
+
identity: {
|
|
2611
|
+
dev: BigInt(cleanupIdentity.dev),
|
|
2612
|
+
ino: BigInt(cleanupIdentity.ino),
|
|
2613
|
+
size: BigInt(cleanupIdentity.size),
|
|
2614
|
+
mtimeNs: BigInt(cleanupIdentity.mtimeNs),
|
|
2615
|
+
},
|
|
2616
|
+
tree: cleanupTree,
|
|
2617
|
+
},
|
|
2618
|
+
path.dirname(source.path),
|
|
2619
|
+
)
|
|
2620
|
+
)
|
|
2621
|
+
throw new Error("durability_failed");
|
|
2622
|
+
} else {
|
|
2623
|
+
throw new Error("durability_failed");
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
|
|
1749
2627
|
if (fs.existsSync(quarantine.path)) {
|
|
1750
|
-
const existing = fs.lstatSync(quarantine.path, { bigint: true });
|
|
1751
2628
|
if (
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2629
|
+
matchesMigrationArtifactRoot(
|
|
2630
|
+
quarantine.path,
|
|
2631
|
+
{
|
|
2632
|
+
...artifactIdentity,
|
|
2633
|
+
},
|
|
2634
|
+
expectedTree,
|
|
2635
|
+
)
|
|
2636
|
+
) {
|
|
2637
|
+
assertPreparedTree(quarantine.path);
|
|
2638
|
+
}
|
|
2639
|
+
// A changed source pathname is independent retained authority. Do not replace
|
|
2640
|
+
// it with the detached original; retain both roots for recovery.
|
|
1761
2641
|
return;
|
|
1762
2642
|
}
|
|
1763
2643
|
assertPreparedTree(quarantine.detachedPath);
|
|
1764
2644
|
const result = native.exactRestore(quarantine.detachedPath, quarantine.path, {
|
|
1765
|
-
|
|
1766
|
-
ino: BigInt(identity.ino),
|
|
1767
|
-
size: BigInt(identity.size),
|
|
1768
|
-
mtimeNs: BigInt(identity.mtimeNs),
|
|
2645
|
+
...artifactIdentity,
|
|
1769
2646
|
directory: true,
|
|
1770
2647
|
});
|
|
1771
2648
|
if (!result.ok) throw new Error("durability_failed");
|
|
1772
2649
|
}
|
|
1773
2650
|
|
|
1774
|
-
function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot): void {
|
|
2651
|
+
function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot, cleanup?: SourceArtifactCleanup): void {
|
|
2652
|
+
if (cleanup && !cleanupAuthorityMatches(cleanup, path.dirname(detached.originalPath)))
|
|
2653
|
+
throw new Error("durability_failed");
|
|
1775
2654
|
const result = native.exactRestore(detached.detachedPath, detached.originalPath, {
|
|
1776
2655
|
...detached.identity,
|
|
1777
2656
|
directory: true,
|
|
@@ -1785,6 +2664,8 @@ async function copyArtifacts(
|
|
|
1785
2664
|
destinationTranscript: string,
|
|
1786
2665
|
manifest: readonly ArtifactManifestEntry[],
|
|
1787
2666
|
lock: ManagedStorageLock,
|
|
2667
|
+
expectedCandidate: ManagedCandidate,
|
|
2668
|
+
expectedIdentity: ResumeSessionIdentity,
|
|
1788
2669
|
sourceRootOverride?: string,
|
|
1789
2670
|
): Promise<void> {
|
|
1790
2671
|
const root = scopeRoot(scope);
|
|
@@ -1792,33 +2673,41 @@ async function copyArtifacts(
|
|
|
1792
2673
|
const sourceRoot = sourceRootOverride ?? sourceTranscript.slice(0, -6);
|
|
1793
2674
|
if (!manifestMatches(sourceTranscript, manifest, sourceRoot)) throw new Error("source_changed");
|
|
1794
2675
|
const destinationRoot = destinationTranscript.slice(0, -6);
|
|
1795
|
-
for (
|
|
2676
|
+
for (let start = 0; start < manifest.length; start += MANAGED_ARTIFACT_COPY_BATCH_SIZE) {
|
|
2677
|
+
const batch = manifest.slice(start, start + MANAGED_ARTIFACT_COPY_BATCH_SIZE);
|
|
2678
|
+
revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
|
|
1796
2679
|
lock.assertOwned();
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
if (entry.
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2680
|
+
for (const entry of batch) {
|
|
2681
|
+
const source = path.join(sourceRoot, entry.path);
|
|
2682
|
+
const destination = path.join(destinationRoot, entry.path);
|
|
2683
|
+
if (entry.kind === "directory") {
|
|
2684
|
+
if (entry.path === "") {
|
|
2685
|
+
ensureManagedDirectory(destinationRoot, root);
|
|
2686
|
+
} else {
|
|
2687
|
+
ensureManagedDirectory(destination, root);
|
|
2688
|
+
}
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
const snapshot = captureManagedFileNoFollow(source);
|
|
2692
|
+
if (
|
|
2693
|
+
snapshot.bytes.byteLength !== entry.size ||
|
|
2694
|
+
createHash("sha256").update(snapshot.bytes).digest("hex") !== entry.sha256
|
|
2695
|
+
)
|
|
2696
|
+
throw new Error("source_changed");
|
|
2697
|
+
try {
|
|
2698
|
+
await copyManagedFileNoReplace(source, destination, snapshot, root);
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2701
|
+
}
|
|
2702
|
+
lock.assertOwned();
|
|
2703
|
+
const copied = captureManagedFileNoFollow(destination);
|
|
2704
|
+
if (
|
|
2705
|
+
copied.bytes.byteLength !== entry.size ||
|
|
2706
|
+
createHash("sha256").update(copied.bytes).digest("hex") !== entry.sha256
|
|
2707
|
+
)
|
|
2708
|
+
throw new Error("durability_failed");
|
|
1814
2709
|
}
|
|
1815
|
-
|
|
1816
|
-
const copied = captureManagedFileNoFollow(destination);
|
|
1817
|
-
if (
|
|
1818
|
-
copied.bytes.byteLength !== entry.size ||
|
|
1819
|
-
createHash("sha256").update(copied.bytes).digest("hex") !== entry.sha256
|
|
1820
|
-
)
|
|
1821
|
-
throw new Error("durability_failed");
|
|
2710
|
+
if (start + batch.length < manifest.length) await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
1822
2711
|
}
|
|
1823
2712
|
if (!manifestMatches(sourceTranscript, manifest, sourceRoot) || !manifestMatches(destinationTranscript, manifest))
|
|
1824
2713
|
throw new Error("durability_failed");
|
|
@@ -1827,7 +2716,7 @@ async function copyArtifacts(
|
|
|
1827
2716
|
function migrationReceipt(
|
|
1828
2717
|
scope: ManagedScope,
|
|
1829
2718
|
lock: ManagedStorageLock,
|
|
1830
|
-
state: "prepared" | "published" | "committed",
|
|
2719
|
+
state: "prepared" | "artifact_detached" | "published" | "committed",
|
|
1831
2720
|
source: ManagedCandidate,
|
|
1832
2721
|
destination: ManagedCandidate | { path: string; sessionId: string; cwd: string },
|
|
1833
2722
|
manifest: readonly ArtifactManifestEntry[],
|
|
@@ -1836,7 +2725,10 @@ function migrationReceipt(
|
|
|
1836
2725
|
detachedPath: string;
|
|
1837
2726
|
identity: DetachedArtifactRoot["identity"];
|
|
1838
2727
|
tree: NativeDirectoryTreeSnapshot;
|
|
2728
|
+
role?: "detached_artifact_root";
|
|
1839
2729
|
},
|
|
2730
|
+
sourceArtifactCleanup?: SourceArtifactCleanup,
|
|
2731
|
+
detachOutcome?: "clean" | "cleanup_pending",
|
|
1840
2732
|
): Uint8Array {
|
|
1841
2733
|
lock.assertOwned();
|
|
1842
2734
|
const destinationRecord =
|
|
@@ -1854,16 +2746,27 @@ function migrationReceipt(
|
|
|
1854
2746
|
header: { id: destination.sessionId, cwd: destination.cwd },
|
|
1855
2747
|
};
|
|
1856
2748
|
return new TextEncoder().encode(
|
|
1857
|
-
`${JSON.stringify({ schemaVersion: 2, state, policy: "copy-retain", attemptId: lock.attemptId, scope: scopeDigest(scope.platform, scope.canonicalCwd), source: { path: source.path, sessionId: source.sessionId, header: { id: source.sessionId, cwd: source.cwd }, identity: source.identity, sha256: source.identity.sha256 }, destination: destinationRecord, artifactManifest: manifest, ...(sourceArtifactQuarantine ? { sourceArtifactQuarantine } : {}) }, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value))}\n`,
|
|
2749
|
+
`${JSON.stringify({ schemaVersion: 2, state, policy: "copy-retain", attemptId: lock.attemptId, scope: scopeDigest(scope.platform, scope.canonicalCwd), source: { path: source.path, sessionId: source.sessionId, header: { id: source.sessionId, cwd: source.cwd }, identity: source.identity, sha256: source.identity.sha256 }, destination: destinationRecord, artifactManifest: manifest, ...(sourceArtifactQuarantine ? { sourceArtifactQuarantine } : {}), ...(sourceArtifactCleanup ? { sourceArtifactCleanup } : {}), ...(detachOutcome ? { detachOutcome } : {}) }, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value))}\n`,
|
|
1858
2750
|
);
|
|
1859
2751
|
}
|
|
1860
2752
|
|
|
2753
|
+
function detachedReceiptMatches(receipt: string, expected: Uint8Array): boolean {
|
|
2754
|
+
try {
|
|
2755
|
+
return captureManagedFileNoFollow(receipt).bytes.equals(expected);
|
|
2756
|
+
} catch {
|
|
2757
|
+
return false;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
|
|
1861
2761
|
async function removeStagedReceipts(scope: ManagedScope, candidate: ManagedCandidate): Promise<void> {
|
|
1862
|
-
for (const state of ["prepared", "published"] as const) {
|
|
2762
|
+
for (const state of ["prepared", "detached", "published"] as const) {
|
|
1863
2763
|
const pathname = receiptPathFor(scope, candidate, state);
|
|
1864
2764
|
try {
|
|
1865
2765
|
const stat = fs.lstatSync(pathname);
|
|
1866
|
-
if (stat.isFile() || stat.isSymbolicLink())
|
|
2766
|
+
if (stat.isFile() || stat.isSymbolicLink()) {
|
|
2767
|
+
await fs.promises.unlink(pathname);
|
|
2768
|
+
fsyncManagedParent(pathname);
|
|
2769
|
+
}
|
|
1867
2770
|
} catch (error) {
|
|
1868
2771
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1869
2772
|
}
|
|
@@ -1905,6 +2808,7 @@ function receiptPair(scope: ManagedScope, candidate: ManagedCandidate): ManagedC
|
|
|
1905
2808
|
}
|
|
1906
2809
|
|
|
1907
2810
|
function validateCandidateForScope(scope: ManagedScope, candidate: ManagedCandidate): ManagedCandidate | undefined {
|
|
2811
|
+
scopeRoot(scope);
|
|
1908
2812
|
const inspected = inspectCandidate(candidate.path, candidate.provenance);
|
|
1909
2813
|
if ("code" in inspected || !sameCandidate(inspected, candidate)) return undefined;
|
|
1910
2814
|
const identity = identityFor(inspected.cwd);
|
|
@@ -1914,12 +2818,16 @@ function validateCandidateForScope(scope: ManagedScope, candidate: ManagedCandid
|
|
|
1914
2818
|
}
|
|
1915
2819
|
|
|
1916
2820
|
/** Resume tombstoned cleanup under its original operation lease without restoring retired candidates. */
|
|
1917
|
-
export async function reconcileManagedTombstones(
|
|
2821
|
+
export async function reconcileManagedTombstones(
|
|
2822
|
+
scope: ManagedScope,
|
|
2823
|
+
expectedCandidate?: ManagedCandidate,
|
|
2824
|
+
): Promise<void> {
|
|
1918
2825
|
const directory = path.join(managedInternalDirectory(scope), MANAGED_TOMBSTONES_DIRECTORY);
|
|
1919
2826
|
for (const name of fs.readdirSync(directory)) {
|
|
1920
2827
|
const tombstone = path.join(directory, name);
|
|
1921
2828
|
const targets = retiredTargets(scope, tombstone);
|
|
1922
2829
|
if (!targets) continue;
|
|
2830
|
+
if (targets.every(target => cleanupCompleted(scope, tombstone, target))) continue;
|
|
1923
2831
|
let lock: ManagedStorageLock | undefined;
|
|
1924
2832
|
try {
|
|
1925
2833
|
lock = await acquireManagedLock(
|
|
@@ -1932,98 +2840,148 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
1932
2840
|
for (const target of lockedTargets) {
|
|
1933
2841
|
lock.assertOwned();
|
|
1934
2842
|
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
1935
|
-
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
1936
|
-
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
1937
2843
|
try {
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
observedPending
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
sessionId: target.sessionId,
|
|
1969
|
-
cwd: target.cwd,
|
|
1970
|
-
transcriptIdentity: target.identity,
|
|
1971
|
-
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
1972
|
-
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
1973
|
-
detachedArtifactsPath:
|
|
1974
|
-
active.detachedArtifactsPath ??
|
|
1975
|
-
observedPending?.detachedArtifactsPath ??
|
|
1976
|
-
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
1977
|
-
detachedTranscriptPath:
|
|
1978
|
-
active.detachedTranscriptPath ??
|
|
1979
|
-
observedPending?.detachedTranscriptPath ??
|
|
1980
|
-
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
1981
|
-
? pending.plannedTranscriptPath
|
|
1982
|
-
: undefined) ??
|
|
1983
|
-
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
1984
|
-
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
1985
|
-
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
1986
|
-
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
1987
|
-
? { artifactsRemoved: true as const }
|
|
1988
|
-
: {}),
|
|
1989
|
-
});
|
|
1990
|
-
if (deletion.kind === "artifacts_removed") {
|
|
1991
|
-
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
1992
|
-
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2844
|
+
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
2845
|
+
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
2846
|
+
try {
|
|
2847
|
+
fs.lstatSync(target.path);
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
2850
|
+
if (!observedPending) continue;
|
|
2851
|
+
if (
|
|
2852
|
+
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2853
|
+
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2854
|
+
) {
|
|
2855
|
+
fsyncManagedParent(target.path);
|
|
2856
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
} else throw error;
|
|
2860
|
+
}
|
|
2861
|
+
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
2862
|
+
if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
|
|
2863
|
+
const discoveredDetach =
|
|
2864
|
+
!!observedPending &&
|
|
2865
|
+
(observedPending.detachedArtifactsPath !== pending?.detachedArtifactsPath ||
|
|
2866
|
+
observedPending.detachedTranscriptPath !== pending?.detachedTranscriptPath);
|
|
2867
|
+
const active =
|
|
2868
|
+
discoveredDetach || (observedPending && requiresFreshCleanupPlan(observedPending))
|
|
2869
|
+
? nextCleanupReceipt(target, observedPending)
|
|
2870
|
+
: (observedPending ?? nextCleanupReceipt(target, undefined));
|
|
2871
|
+
if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
|
|
2872
|
+
await publishCleanupPending(scope, tombstone, active, lock);
|
|
2873
|
+
let deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
1993
2874
|
sessionsRoot: scope.sessionsRoot,
|
|
1994
2875
|
transcriptPath: target.path,
|
|
1995
2876
|
sessionId: target.sessionId,
|
|
1996
2877
|
cwd: target.cwd,
|
|
1997
2878
|
transcriptIdentity: target.identity,
|
|
2879
|
+
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
2880
|
+
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
2881
|
+
detachedArtifactsPath:
|
|
2882
|
+
active.detachedArtifactsPath ??
|
|
2883
|
+
observedPending?.detachedArtifactsPath ??
|
|
2884
|
+
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
2885
|
+
detachedTranscriptPath:
|
|
2886
|
+
active.detachedTranscriptPath ??
|
|
2887
|
+
observedPending?.detachedTranscriptPath ??
|
|
2888
|
+
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
2889
|
+
? pending.plannedTranscriptPath
|
|
2890
|
+
: undefined) ??
|
|
2891
|
+
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
2892
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2893
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2894
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2895
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2896
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2897
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
1998
2898
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
1999
2899
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2000
|
-
|
|
2900
|
+
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
2901
|
+
? { artifactsRemoved: true as const }
|
|
2902
|
+
: {}),
|
|
2001
2903
|
});
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2904
|
+
if (deletion.kind === "artifacts_removed") {
|
|
2905
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
2906
|
+
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2907
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2908
|
+
transcriptPath: target.path,
|
|
2909
|
+
sessionId: target.sessionId,
|
|
2910
|
+
cwd: target.cwd,
|
|
2911
|
+
transcriptIdentity: target.identity,
|
|
2912
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2913
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2914
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
2915
|
+
|
|
2916
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2917
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2918
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2919
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2920
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2921
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2922
|
+
artifactsRemoved: true,
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
if (deletion.kind === "cleanup_pending") {
|
|
2926
|
+
const retry = nextCleanupReceipt(target, active);
|
|
2927
|
+
await publishCleanupPending(
|
|
2928
|
+
scope,
|
|
2929
|
+
tombstone,
|
|
2930
|
+
{
|
|
2931
|
+
...retry,
|
|
2932
|
+
expectedArtifactsIdentity:
|
|
2933
|
+
deletion.phase === "artifacts"
|
|
2934
|
+
? deletion.artifactsIdentity
|
|
2935
|
+
: active.expectedArtifactsIdentity,
|
|
2936
|
+
expectedArtifactsTree:
|
|
2937
|
+
deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2938
|
+
detachedArtifactsPath:
|
|
2939
|
+
deletion.phase === "artifacts"
|
|
2940
|
+
? deletion.detachedArtifactsPath
|
|
2941
|
+
: active.detachedArtifactsPath,
|
|
2942
|
+
detachedTranscriptPath:
|
|
2943
|
+
deletion.phase === "transcript"
|
|
2944
|
+
? deletion.detachedTranscriptPath
|
|
2945
|
+
: active.detachedTranscriptPath,
|
|
2946
|
+
retainedArtifactsSuccessorPath:
|
|
2947
|
+
deletion.phase === "artifacts"
|
|
2948
|
+
? deletion.retainedSuccessorPath
|
|
2949
|
+
: active.retainedArtifactsSuccessorPath,
|
|
2950
|
+
retainedArtifactsPlaceholderPath:
|
|
2951
|
+
deletion.phase === "artifacts"
|
|
2952
|
+
? deletion.retainedPlaceholderPath
|
|
2953
|
+
: active.retainedArtifactsPlaceholderPath,
|
|
2954
|
+
retainedArtifactsUnknownPath:
|
|
2955
|
+
deletion.phase === "artifacts"
|
|
2956
|
+
? deletion.retainedUnknownPath
|
|
2957
|
+
: active.retainedArtifactsUnknownPath,
|
|
2958
|
+
retainedTranscriptSuccessorPath:
|
|
2959
|
+
deletion.phase === "transcript"
|
|
2960
|
+
? deletion.retainedSuccessorPath
|
|
2961
|
+
: active.retainedTranscriptSuccessorPath,
|
|
2962
|
+
retainedTranscriptPlaceholderPath:
|
|
2963
|
+
deletion.phase === "transcript"
|
|
2964
|
+
? deletion.retainedPlaceholderPath
|
|
2965
|
+
: active.retainedTranscriptPlaceholderPath,
|
|
2966
|
+
retainedTranscriptUnknownPath:
|
|
2967
|
+
deletion.phase === "transcript"
|
|
2968
|
+
? deletion.retainedUnknownPath
|
|
2969
|
+
: active.retainedTranscriptUnknownPath,
|
|
2970
|
+
},
|
|
2971
|
+
lock,
|
|
2972
|
+
);
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2975
|
+
fsyncManagedParent(target.path);
|
|
2976
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2977
|
+
} catch (error) {
|
|
2978
|
+
if (!expectedCandidate || target.sessionId === expectedCandidate.sessionId) throw error;
|
|
2979
|
+
logger.warn("Tombstone reconciliation failed for one target; will retry on a future scope open", {
|
|
2007
2980
|
tombstone,
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
2012
|
-
expectedArtifactsTree:
|
|
2013
|
-
deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2014
|
-
detachedArtifactsPath:
|
|
2015
|
-
deletion.phase === "artifacts" ? deletion.detachedArtifactsPath : active.detachedArtifactsPath,
|
|
2016
|
-
detachedTranscriptPath:
|
|
2017
|
-
deletion.phase === "transcript"
|
|
2018
|
-
? deletion.detachedTranscriptPath
|
|
2019
|
-
: active.detachedTranscriptPath,
|
|
2020
|
-
},
|
|
2021
|
-
lock,
|
|
2022
|
-
);
|
|
2023
|
-
throw new Error("durability_failed");
|
|
2981
|
+
sessionId: target.sessionId,
|
|
2982
|
+
error: String(error),
|
|
2983
|
+
});
|
|
2024
2984
|
}
|
|
2025
|
-
fsyncManagedParent(target.path);
|
|
2026
|
-
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
2027
2985
|
}
|
|
2028
2986
|
} finally {
|
|
2029
2987
|
if (lock) await lock.release().catch(() => undefined);
|
|
@@ -2032,24 +2990,45 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2032
2990
|
}
|
|
2033
2991
|
|
|
2034
2992
|
/** Create the v2 binding and private write protocol directories before managed writes. */
|
|
2035
|
-
export async function prepareManagedSessionScopeForWrite(
|
|
2036
|
-
|
|
2993
|
+
export async function prepareManagedSessionScopeForWrite(
|
|
2994
|
+
scope: ManagedScope,
|
|
2995
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
2996
|
+
authority?: ManagedCandidateWriteAuthority,
|
|
2997
|
+
expectedCandidate?: ManagedCandidate,
|
|
2998
|
+
expectedIdentity?: ResumeSessionIdentity,
|
|
2999
|
+
): Promise<ManagedScopeResolution> {
|
|
3000
|
+
if (expectedCandidate && expectedIdentity) revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
|
|
3001
|
+
if (authority) bindManagedWriteAuthority(scope, authority);
|
|
3002
|
+
const prepared = await ensureManagedScope(scope, policy);
|
|
2037
3003
|
if (prepared.kind === "error") return prepared;
|
|
2038
3004
|
try {
|
|
2039
3005
|
const internal = managedInternalDirectory(scope);
|
|
2040
3006
|
const root = scopeRoot(scope);
|
|
2041
|
-
ensureManagedDirectory(internal, root);
|
|
2042
|
-
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
|
|
2043
|
-
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
|
|
2044
|
-
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
|
|
2045
|
-
await reconcileManagedTombstones(scope);
|
|
3007
|
+
ensureManagedDirectory(internal, root, policy);
|
|
3008
|
+
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root, policy);
|
|
3009
|
+
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root, policy);
|
|
3010
|
+
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
|
|
3011
|
+
await reconcileManagedTombstones(scope, expectedCandidate);
|
|
2046
3012
|
return { kind: "resolved", scope };
|
|
2047
3013
|
} catch (error) {
|
|
2048
|
-
|
|
3014
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
3015
|
+
const message =
|
|
3016
|
+
publication?.classification ??
|
|
3017
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
3018
|
+
const code =
|
|
3019
|
+
message === "atomic_unavailable" ||
|
|
3020
|
+
message === "invalid_request" ||
|
|
3021
|
+
message === "durability_failed" ||
|
|
3022
|
+
message === "durability_not_provable"
|
|
3023
|
+
? message
|
|
3024
|
+
: "binding_invalid";
|
|
2049
3025
|
return {
|
|
2050
3026
|
kind: "error",
|
|
2051
|
-
code
|
|
2052
|
-
message
|
|
3027
|
+
code,
|
|
3028
|
+
message,
|
|
3029
|
+
...(publication
|
|
3030
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
3031
|
+
: { cause: { classification: code } }),
|
|
2053
3032
|
};
|
|
2054
3033
|
}
|
|
2055
3034
|
}
|
|
@@ -2061,28 +3040,45 @@ export async function prepareManagedSessionScopeForWrite(scope: ManagedScope): P
|
|
|
2061
3040
|
export async function openManagedCandidateForWrite(
|
|
2062
3041
|
scope: ManagedScope,
|
|
2063
3042
|
candidate: ManagedCandidate,
|
|
2064
|
-
|
|
3043
|
+
expectedIdentityOrMigrationPolicy: ResumeSessionIdentity | ManagedMigrationPolicy = "copy-retain",
|
|
3044
|
+
migrationPolicy: ManagedMigrationPolicy = typeof expectedIdentityOrMigrationPolicy === "string"
|
|
3045
|
+
? expectedIdentityOrMigrationPolicy
|
|
3046
|
+
: "copy-retain",
|
|
3047
|
+
authority?: ManagedCandidateWriteAuthority,
|
|
2065
3048
|
): Promise<ManagedOpenCandidateResult> {
|
|
3049
|
+
const expectedIdentity =
|
|
3050
|
+
typeof expectedIdentityOrMigrationPolicy === "string" ? candidate.identity : expectedIdentityOrMigrationPolicy;
|
|
2066
3051
|
if (migrationPolicy === "disabled" && candidate.provenance === "legacy")
|
|
2067
3052
|
return {
|
|
2068
3053
|
kind: "error",
|
|
2069
3054
|
code: "legacy_migration_disabled",
|
|
2070
3055
|
message: "Legacy session migration is disabled for this workspace.",
|
|
2071
3056
|
};
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
3057
|
+
let prepared: ManagedScopeResolution;
|
|
3058
|
+
let current: ManagedCandidate;
|
|
3059
|
+
try {
|
|
3060
|
+
prepared = await prepareManagedSessionScopeForWrite(
|
|
3061
|
+
scope,
|
|
3062
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
3063
|
+
authority,
|
|
3064
|
+
candidate,
|
|
3065
|
+
expectedIdentity,
|
|
3066
|
+
);
|
|
3067
|
+
if (prepared.kind === "error")
|
|
3068
|
+
return {
|
|
3069
|
+
kind: "error",
|
|
3070
|
+
code: expectedFailure(new Error(prepared.code)),
|
|
3071
|
+
|
|
3072
|
+
message: prepared.message,
|
|
3073
|
+
};
|
|
3074
|
+
current = revalidatePickerConsent(scope, candidate, expectedIdentity);
|
|
3075
|
+
} catch (error) {
|
|
2081
3076
|
return {
|
|
2082
3077
|
kind: "error",
|
|
2083
|
-
code:
|
|
2084
|
-
message:
|
|
3078
|
+
code: expectedFailure(error),
|
|
3079
|
+
message: error instanceof Error ? error.message : "Managed migration failed.",
|
|
2085
3080
|
};
|
|
3081
|
+
}
|
|
2086
3082
|
if (isRetired(scope, current))
|
|
2087
3083
|
return { kind: "error", code: "migration_retired", message: "The managed session has been retired." };
|
|
2088
3084
|
if (current.provenance === "v2") return { kind: "opened", path: current.path, candidate: current, migrated: false };
|
|
@@ -2091,11 +3087,15 @@ export async function openManagedCandidateForWrite(
|
|
|
2091
3087
|
const internal = managedInternalDirectory(scope);
|
|
2092
3088
|
let lock: ManagedStorageLock | undefined;
|
|
2093
3089
|
let detachedArtifacts: DetachedArtifactRoot | undefined;
|
|
3090
|
+
let sourceArtifactCleanup: SourceArtifactCleanup | undefined;
|
|
3091
|
+
|
|
2094
3092
|
try {
|
|
3093
|
+
revalidatePickerConsent(scope, current, expectedIdentity);
|
|
2095
3094
|
lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation, scopeRoot(scope));
|
|
2096
|
-
const
|
|
2097
|
-
|
|
2098
|
-
|
|
3095
|
+
const heldLock = lock;
|
|
3096
|
+
|
|
3097
|
+
const afterLock = revalidatePickerConsent(scope, current, expectedIdentity);
|
|
3098
|
+
|
|
2099
3099
|
const listing = listManagedCandidates(scope);
|
|
2100
3100
|
if (listing.kind === "error") return { kind: "error", code: "binding_invalid", message: listing.message };
|
|
2101
3101
|
const destination = path.join(scope.directoryPath, path.basename(afterLock.path));
|
|
@@ -2107,11 +3107,29 @@ export async function openManagedCandidateForWrite(
|
|
|
2107
3107
|
code: "destination_conflict",
|
|
2108
3108
|
message: "A distinct v2 transcript already owns this session id.",
|
|
2109
3109
|
};
|
|
2110
|
-
|
|
2111
|
-
|
|
3110
|
+
|
|
3111
|
+
scopeRoot(scope);
|
|
3112
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3113
|
+
restorePreparedArtifactRoot(scope, afterLock, heldLock);
|
|
3114
|
+
scopeRoot(scope);
|
|
3115
|
+
|
|
3116
|
+
let sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
|
|
3117
|
+
if (
|
|
3118
|
+
sourceSnapshot.identity.dev !== afterLock.identity.dev ||
|
|
3119
|
+
sourceSnapshot.identity.ino !== afterLock.identity.ino ||
|
|
3120
|
+
sourceSnapshot.identity.size !== afterLock.identity.size ||
|
|
3121
|
+
sourceSnapshot.identity.mtimeNs !== afterLock.identity.mtimeNs ||
|
|
3122
|
+
sourceSnapshot.identity.sha256 !== afterLock.identity.sha256 ||
|
|
3123
|
+
sourceSnapshot.bytes.byteLength !== afterLock.identity.size
|
|
3124
|
+
)
|
|
3125
|
+
throw new Error("source_changed");
|
|
2112
3126
|
let manifest: readonly ArtifactManifestEntry[] = [];
|
|
2113
3127
|
const artifactPlan = planArtifactRootForMigration(afterLock.path, operation);
|
|
2114
3128
|
const intendedDestination = { path: destination, sessionId: afterLock.sessionId, cwd: afterLock.cwd };
|
|
3129
|
+
const assertPublicationConsent = (): void => {
|
|
3130
|
+
heldLock.assertOwned();
|
|
3131
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3132
|
+
};
|
|
2115
3133
|
if (existing && existing.identity.sha256 !== afterLock.identity.sha256) {
|
|
2116
3134
|
return {
|
|
2117
3135
|
kind: "error",
|
|
@@ -2126,8 +3144,10 @@ export async function openManagedCandidateForWrite(
|
|
|
2126
3144
|
message: "The migration destination already exists without validated ownership.",
|
|
2127
3145
|
};
|
|
2128
3146
|
}
|
|
3147
|
+
|
|
2129
3148
|
const preparedReceipt = receiptPathFor(scope, afterLock, "prepared");
|
|
2130
3149
|
try {
|
|
3150
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2131
3151
|
lock.assertOwned();
|
|
2132
3152
|
await publishManagedFileNoReplace(
|
|
2133
3153
|
preparedReceipt,
|
|
@@ -2147,27 +3167,97 @@ export async function openManagedCandidateForWrite(
|
|
|
2147
3167
|
}
|
|
2148
3168
|
: undefined,
|
|
2149
3169
|
),
|
|
2150
|
-
|
|
3170
|
+
assertPublicationConsent,
|
|
3171
|
+
scopeRoot(scope),
|
|
2151
3172
|
);
|
|
2152
3173
|
} catch (error) {
|
|
2153
3174
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2154
3175
|
}
|
|
3176
|
+
|
|
3177
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2155
3178
|
lock.assertOwned();
|
|
2156
3179
|
if (!preparedReceiptMatches(preparedReceipt, scope, afterLock, intendedDestination, artifactPlan))
|
|
2157
3180
|
throw new Error("durability_failed");
|
|
2158
3181
|
if (artifactPlan && fs.existsSync(artifactPlan.detachedPath)) throw new Error("destination_conflict");
|
|
2159
|
-
|
|
3182
|
+
|
|
3183
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3184
|
+
|
|
3185
|
+
scopeRoot(scope);
|
|
3186
|
+
|
|
3187
|
+
if (artifactPlan) {
|
|
3188
|
+
const detached = detachArtifactRootForMigration(artifactPlan);
|
|
3189
|
+
detachedArtifacts = detached.detached;
|
|
3190
|
+
sourceArtifactCleanup = detached.detachOutcome === "cleanup_pending" ? detached.cleanup : undefined;
|
|
3191
|
+
const detachedReceipt = receiptPathFor(scope, afterLock, "detached");
|
|
3192
|
+
const detachedRecord = migrationReceipt(
|
|
3193
|
+
scope,
|
|
3194
|
+
lock,
|
|
3195
|
+
"artifact_detached",
|
|
3196
|
+
afterLock,
|
|
3197
|
+
intendedDestination,
|
|
3198
|
+
[],
|
|
3199
|
+
{
|
|
3200
|
+
path: detachedArtifacts.originalPath,
|
|
3201
|
+
detachedPath: detachedArtifacts.detachedPath,
|
|
3202
|
+
identity: detachedArtifacts.identity,
|
|
3203
|
+
tree: detachedArtifacts.tree,
|
|
3204
|
+
role: "detached_artifact_root",
|
|
3205
|
+
},
|
|
3206
|
+
sourceArtifactCleanup,
|
|
3207
|
+
detached.detachOutcome,
|
|
3208
|
+
);
|
|
3209
|
+
try {
|
|
3210
|
+
await publishManagedFileNoReplace(
|
|
3211
|
+
detachedReceipt,
|
|
3212
|
+
detachedRecord,
|
|
3213
|
+
assertPublicationConsent,
|
|
3214
|
+
scopeRoot(scope),
|
|
3215
|
+
);
|
|
3216
|
+
} catch (error) {
|
|
3217
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
3218
|
+
}
|
|
3219
|
+
if (!detachedReceiptMatches(detachedReceipt, detachedRecord)) throw new Error("durability_failed");
|
|
3220
|
+
}
|
|
2160
3221
|
manifest = detachedArtifacts ? artifactManifestFromSnapshot(detachedArtifacts.tree) : [];
|
|
2161
|
-
|
|
3222
|
+
|
|
3223
|
+
await copyArtifacts(
|
|
3224
|
+
scope,
|
|
3225
|
+
afterLock.path,
|
|
3226
|
+
destination,
|
|
3227
|
+
manifest,
|
|
3228
|
+
lock,
|
|
3229
|
+
afterLock,
|
|
3230
|
+
expectedIdentity,
|
|
3231
|
+
detachedArtifacts?.detachedPath,
|
|
3232
|
+
);
|
|
3233
|
+
|
|
3234
|
+
if (!existing) {
|
|
3235
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3236
|
+
lock.assertOwned();
|
|
3237
|
+
sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
|
|
3238
|
+
if (
|
|
3239
|
+
sourceSnapshot.identity.dev !== afterLock.identity.dev ||
|
|
3240
|
+
sourceSnapshot.identity.ino !== afterLock.identity.ino ||
|
|
3241
|
+
sourceSnapshot.identity.size !== afterLock.identity.size ||
|
|
3242
|
+
sourceSnapshot.identity.mtimeNs !== afterLock.identity.mtimeNs ||
|
|
3243
|
+
sourceSnapshot.identity.sha256 !== afterLock.identity.sha256 ||
|
|
3244
|
+
sourceSnapshot.bytes.byteLength !== afterLock.identity.size
|
|
3245
|
+
)
|
|
3246
|
+
throw new Error("source_changed");
|
|
3247
|
+
}
|
|
3248
|
+
|
|
2162
3249
|
if (!existing) {
|
|
2163
3250
|
try {
|
|
3251
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2164
3252
|
lock.assertOwned();
|
|
2165
3253
|
await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot, scopeRoot(scope));
|
|
2166
3254
|
} catch (error) {
|
|
2167
3255
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2168
3256
|
}
|
|
2169
3257
|
}
|
|
3258
|
+
|
|
2170
3259
|
// Artifact files, directories, and the transcript must be durable before a receipt can grant shadow authority.
|
|
3260
|
+
scopeRoot(scope);
|
|
2171
3261
|
if (manifest.length > 0) fsyncManagedArtifactTree(destination.slice(0, -6));
|
|
2172
3262
|
lock.assertOwned();
|
|
2173
3263
|
const migrated = inspectCandidate(destination, "v2");
|
|
@@ -2179,10 +3269,14 @@ export async function openManagedCandidateForWrite(
|
|
|
2179
3269
|
!manifestMatches(destination, manifest)
|
|
2180
3270
|
)
|
|
2181
3271
|
throw new Error("durability_failed");
|
|
3272
|
+
|
|
2182
3273
|
if (detachedArtifacts) {
|
|
2183
|
-
|
|
3274
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3275
|
+
scopeRoot(scope);
|
|
3276
|
+
restoreDetachedArtifactRoot(detachedArtifacts, sourceArtifactCleanup);
|
|
2184
3277
|
detachedArtifacts = undefined;
|
|
2185
3278
|
}
|
|
3279
|
+
|
|
2186
3280
|
const latest = validateCandidateForScope(scope, afterLock);
|
|
2187
3281
|
if (!latest || !sameCandidate(latest, afterLock) || !manifestMatches(afterLock.path, manifest))
|
|
2188
3282
|
return {
|
|
@@ -2190,19 +3284,40 @@ export async function openManagedCandidateForWrite(
|
|
|
2190
3284
|
code: "source_changed",
|
|
2191
3285
|
message: "The legacy candidate or its artifacts changed before migration commit.",
|
|
2192
3286
|
};
|
|
3287
|
+
|
|
2193
3288
|
for (const state of ["published", "committed"] as const) {
|
|
2194
3289
|
const receipt = receiptPathFor(scope, afterLock, state);
|
|
2195
3290
|
try {
|
|
3291
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2196
3292
|
lock.assertOwned();
|
|
2197
3293
|
await publishManagedFileNoReplace(
|
|
2198
3294
|
receipt,
|
|
2199
|
-
migrationReceipt(
|
|
2200
|
-
|
|
3295
|
+
migrationReceipt(
|
|
3296
|
+
scope,
|
|
3297
|
+
lock,
|
|
3298
|
+
state,
|
|
3299
|
+
afterLock,
|
|
3300
|
+
migrated,
|
|
3301
|
+
manifest,
|
|
3302
|
+
artifactPlan
|
|
3303
|
+
? {
|
|
3304
|
+
path: artifactPlan.originalPath,
|
|
3305
|
+
detachedPath: artifactPlan.detachedPath,
|
|
3306
|
+
identity: artifactPlan.identity,
|
|
3307
|
+
tree: artifactPlan.tree,
|
|
3308
|
+
role: "detached_artifact_root",
|
|
3309
|
+
}
|
|
3310
|
+
: undefined,
|
|
3311
|
+
sourceArtifactCleanup,
|
|
3312
|
+
),
|
|
3313
|
+
assertPublicationConsent,
|
|
3314
|
+
scopeRoot(scope),
|
|
2201
3315
|
);
|
|
2202
3316
|
} catch (error) {
|
|
2203
3317
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2204
3318
|
}
|
|
2205
3319
|
}
|
|
3320
|
+
|
|
2206
3321
|
const receipt = receiptPathFor(scope, afterLock);
|
|
2207
3322
|
lock.assertOwned();
|
|
2208
3323
|
if (!receiptMatches(receipt, afterLock, migrated, scope))
|
|
@@ -2211,6 +3326,9 @@ export async function openManagedCandidateForWrite(
|
|
|
2211
3326
|
code: "durability_failed",
|
|
2212
3327
|
message: "The migration receipt does not bind the copied v2 transcript and artifacts.",
|
|
2213
3328
|
};
|
|
3329
|
+
|
|
3330
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3331
|
+
scopeRoot(scope);
|
|
2214
3332
|
await removeStagedReceipts(scope, afterLock);
|
|
2215
3333
|
return {
|
|
2216
3334
|
kind: "opened",
|
|
@@ -2220,7 +3338,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2220
3338
|
};
|
|
2221
3339
|
} catch (error) {
|
|
2222
3340
|
try {
|
|
2223
|
-
if (detachedArtifacts) restoreDetachedArtifactRoot(detachedArtifacts);
|
|
3341
|
+
if (detachedArtifacts) restoreDetachedArtifactRoot(detachedArtifacts, sourceArtifactCleanup);
|
|
2224
3342
|
} catch {
|
|
2225
3343
|
return {
|
|
2226
3344
|
kind: "error",
|
|
@@ -2229,7 +3347,16 @@ export async function openManagedCandidateForWrite(
|
|
|
2229
3347
|
};
|
|
2230
3348
|
}
|
|
2231
3349
|
const code = expectedFailure(error);
|
|
2232
|
-
return {
|
|
3350
|
+
return {
|
|
3351
|
+
kind: "error",
|
|
3352
|
+
code,
|
|
3353
|
+
message:
|
|
3354
|
+
code === "artifact_capacity_exceeded"
|
|
3355
|
+
? `Legacy session artifacts exceed the migration capacity (${MANAGED_ARTIFACT_MAX_FILES.toLocaleString()} files or ${MANAGED_ARTIFACT_MAX_TOTAL_BYTES / 1024 / 1024} MiB).`
|
|
3356
|
+
: error instanceof Error
|
|
3357
|
+
? error.message
|
|
3358
|
+
: "Managed migration failed.",
|
|
3359
|
+
};
|
|
2233
3360
|
} finally {
|
|
2234
3361
|
if (lock) await lock.release().catch(() => undefined);
|
|
2235
3362
|
}
|
|
@@ -2244,7 +3371,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
2244
3371
|
if (prepared.kind === "error")
|
|
2245
3372
|
return {
|
|
2246
3373
|
kind: "error",
|
|
2247
|
-
code: prepared.code
|
|
3374
|
+
code: expectedFailure(new Error(prepared.code)),
|
|
2248
3375
|
message: prepared.message,
|
|
2249
3376
|
};
|
|
2250
3377
|
const current = validateCandidateForScope(scope, candidate);
|
|
@@ -2304,8 +3431,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
2304
3431
|
if (!observedPending) continue;
|
|
2305
3432
|
if (
|
|
2306
3433
|
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2307
|
-
|
|
2308
|
-
!fs.existsSync(observedPending.plannedTranscriptPath)
|
|
3434
|
+
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2309
3435
|
) {
|
|
2310
3436
|
fsyncManagedParent(target.path);
|
|
2311
3437
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -2340,6 +3466,12 @@ export async function deleteManagedSessionCandidate(
|
|
|
2340
3466
|
active.detachedTranscriptPath ??
|
|
2341
3467
|
observedPending?.detachedTranscriptPath ??
|
|
2342
3468
|
(pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
|
|
3469
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3470
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3471
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3472
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3473
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3474
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2343
3475
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2344
3476
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2345
3477
|
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
@@ -2356,6 +3488,13 @@ export async function deleteManagedSessionCandidate(
|
|
|
2356
3488
|
transcriptIdentity: target.identity,
|
|
2357
3489
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2358
3490
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3491
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3492
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3493
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3494
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3495
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3496
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3497
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2359
3498
|
artifactsRemoved: true,
|
|
2360
3499
|
});
|
|
2361
3500
|
}
|
|
@@ -2371,23 +3510,51 @@ export async function deleteManagedSessionCandidate(
|
|
|
2371
3510
|
)
|
|
2372
3511
|
throw new Error("durability_failed");
|
|
2373
3512
|
const retry = nextCleanupReceipt(target, active);
|
|
2374
|
-
await publishCleanupPending(
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
3513
|
+
await publishCleanupPending(scope, tombstone, cleanupPendingEvidence(retry, active, deletion), lock);
|
|
3514
|
+
if (deletion.phase === "artifacts") {
|
|
3515
|
+
// A typed retained artifact root proves durable canonical absence; advance
|
|
3516
|
+
// to the transcript phase instead of stalling on the retained evidence.
|
|
3517
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
3518
|
+
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
3519
|
+
sessionsRoot: scope.sessionsRoot,
|
|
3520
|
+
transcriptPath: target.path,
|
|
3521
|
+
sessionId: target.sessionId,
|
|
3522
|
+
cwd: target.cwd,
|
|
3523
|
+
transcriptIdentity: target.identity,
|
|
3524
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3525
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3526
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3527
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3528
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3529
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3530
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3531
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3532
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
3533
|
+
artifactsRemoved: true,
|
|
3534
|
+
});
|
|
3535
|
+
if (deletion.kind === "cleanup_pending") {
|
|
3536
|
+
const followup = nextCleanupReceipt(target, active);
|
|
3537
|
+
await publishCleanupPending(
|
|
3538
|
+
scope,
|
|
3539
|
+
tombstone,
|
|
3540
|
+
cleanupPendingEvidence(followup, active, deletion),
|
|
3541
|
+
lock,
|
|
3542
|
+
);
|
|
3543
|
+
if (
|
|
3544
|
+
deletion.phase !== "transcript" ||
|
|
3545
|
+
deletion.detachedTranscriptPath !== active.plannedTranscriptPath
|
|
3546
|
+
)
|
|
3547
|
+
return {
|
|
3548
|
+
kind: "cleanup_pending",
|
|
3549
|
+
tombstonePath: tombstone,
|
|
3550
|
+
phase: deletion.phase,
|
|
3551
|
+
message:
|
|
3552
|
+
"Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
// A typed retained transcript quarantine is durable canonical-absence
|
|
3557
|
+
// evidence — never a terminal byte-deletion claim; complete the receipt.
|
|
2391
3558
|
}
|
|
2392
3559
|
fsyncManagedParent(target.path);
|
|
2393
3560
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|