@gajae-code/coding-agent 0.18.7 → 0.18.9
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 +377 -1
- package/dist/types/async/endpoint-id.d.ts +16 -0
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/async/support.d.ts +0 -14
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/capability/skill.d.ts +2 -0
- package/dist/types/capability/types.d.ts +2 -0
- package/dist/types/cli/public-command-errors.d.ts +3 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/mcp-serve.d.ts +4 -0
- package/dist/types/config/autorouting-tier-map.d.ts +348 -9
- package/dist/types/config/file-lock.d.ts +1 -0
- package/dist/types/config/model-bindings-applier.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -3
- package/dist/types/config/model-registry.d.ts +4 -9
- package/dist/types/config/skill-settings-defaults.d.ts +2 -0
- package/dist/types/cursor-delete-path.d.ts +1 -0
- package/dist/types/cursor-mcp-name.d.ts +8 -0
- package/dist/types/customization/inventory.d.ts +2 -0
- package/dist/types/debug/report-bundle-parent.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
- package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
- package/dist/types/discovery/helpers.d.ts +6 -1
- package/dist/types/discovery/mcp-json.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +18 -0
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/html-export-destination.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
- package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
- package/dist/types/extensibility/skill-management.d.ts +4 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
- package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
- package/dist/types/internal-urls/skill-path.d.ts +1 -0
- package/dist/types/lsp/config.d.ts +8 -5
- package/dist/types/lsp/workspace-path.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
- package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
- package/dist/types/progress/collect-project-progress.d.ts +38 -0
- package/dist/types/progress/progress-contract.d.ts +154 -0
- package/dist/types/progress/progress-snapshot.d.ts +11 -0
- package/dist/types/progress/project-progress.d.ts +141 -0
- package/dist/types/progress/render-progress.d.ts +28 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -1
- package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
- package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk/acp/adapter.d.ts +1 -1
- package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
- package/dist/types/sdk/broker/broker.d.ts +24 -2
- package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -1
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/bus/index.d.ts +12 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +3 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +38 -9
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
- package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +31 -7
- package/dist/types/session/session-storage.d.ts +69 -2
- package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
- package/dist/types/setup/credential-auto-import.d.ts +1 -1
- package/dist/types/setup/credential-import.d.ts +6 -4
- package/dist/types/skill-state/active-state.d.ts +38 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
- package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/isolation-dir.d.ts +26 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/browser/cdp-url.d.ts +2 -0
- package/dist/types/tools/github-side-effect.d.ts +22 -0
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/monitor-mutation.d.ts +9 -0
- package/dist/types/tools/python.d.ts +9 -1
- package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
- package/dist/types/tools/restricted-cwd.d.ts +1 -0
- package/dist/types/tools/todo-write.d.ts +6 -0
- package/dist/types/utils/tools-checksum.d.ts +7 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +21 -0
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/endpoint-id.ts +26 -0
- package/src/async/job-manager.ts +247 -132
- package/src/async/support.ts +0 -25
- package/src/capability/index.ts +15 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +2 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/doctor/repairs.ts +4 -1
- package/src/cli/doctor-supervisor.ts +3 -0
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/public-command-registry.ts +28 -6
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/mcp-serve.ts +7 -0
- package/src/commands/sdk.ts +455 -397
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +316 -79
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-bindings-applier.ts +20 -0
- package/src/config/model-profile-activation.ts +240 -46
- package/src/config/model-registry.ts +47 -153
- package/src/config/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/coordinator-mcp/server.ts +2 -0
- package/src/cursor-delete-path.ts +10 -0
- package/src/cursor-mcp-name.ts +15 -0
- package/src/cursor.ts +13 -2
- package/src/customization/inventory.ts +4 -13
- package/src/dap/config.ts +2 -2
- package/src/debug/report-bundle-parent.ts +22 -0
- package/src/debug/report-bundle.ts +3 -0
- package/src/debug/system-info.ts +13 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
- package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
- package/src/defaults/gjc-defaults.test.ts +18 -1
- package/src/defaults/gjc-defaults.ts +3 -0
- package/src/defaults/gjc-skills.generated.ts +58 -36
- package/src/discovery/agents.ts +6 -1
- package/src/discovery/builtin.ts +13 -1
- package/src/discovery/helpers.ts +171 -29
- package/src/discovery/mcp-json.ts +26 -12
- package/src/discovery/ssh.ts +1 -1
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +796 -154
- package/src/eval/py/kernel.ts +79 -13
- package/src/eval/py/tool-bridge.ts +4 -0
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/html-export-destination.ts +16 -0
- package/src/export/html/index.ts +7 -10
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
- package/src/extensibility/extensions/runner.ts +15 -1
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
- package/src/extensibility/plugins/manager.ts +3 -2
- package/src/extensibility/plugins/marketplace/cache.ts +15 -0
- package/src/extensibility/plugins/marketplace/manager.ts +11 -3
- package/src/extensibility/plugins/plugin-link-path.ts +12 -0
- package/src/extensibility/runtime-skill-discovery.ts +20 -2
- package/src/extensibility/skill-management.ts +9 -2
- package/src/extensibility/skills.ts +6 -1
- package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
- package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
- package/src/gjc-runtime/deep-interview-stage.ts +98 -21
- package/src/gjc-runtime/gc-runtime.ts +206 -9
- package/src/gjc-runtime/launch-worktree.ts +70 -9
- package/src/gjc-runtime/psmux-detect.ts +28 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/session-state-sidecar.ts +3 -2
- package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
- package/src/gjc-runtime/state-argv.ts +2 -0
- package/src/gjc-runtime/state-migrations.ts +91 -29
- package/src/gjc-runtime/state-runtime.ts +3716 -135
- package/src/gjc-runtime/state-writer.ts +105 -16
- package/src/gjc-runtime/tmux-common.ts +8 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
- package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
- package/src/gjc-runtime/workflow-manifest.ts +54 -21
- package/src/harness-control-plane/operate.ts +12 -4
- package/src/harness-control-plane/owner.ts +5 -1
- package/src/hashline/parser.ts +48 -9
- package/src/hashline/recovery.ts +13 -4
- package/src/internal-urls/docs-index.generated.ts +20 -19
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/config.ts +12 -63
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/memories/index.ts +88 -7
- package/src/modes/acp/acp-agent.ts +88 -4
- package/src/modes/components/custom-editor.ts +14 -0
- package/src/modes/components/customization/customization-dashboard.ts +12 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -2
- package/src/modes/components/extensions/state-manager.ts +15 -5
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +1 -4
- package/src/modes/components/provider-onboarding-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +4 -0
- package/src/modes/controllers/event-controller.ts +11 -4
- package/src/modes/controllers/input-controller.ts +76 -31
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
- package/src/modes/controllers/selector-controller.ts +24 -15
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -1
- package/src/modes/runtime-init.ts +4 -0
- package/src/modes/utils/abort-message.ts +3 -1
- package/src/modes/utils/interrupt-ownership.ts +12 -0
- package/src/modes/utils/keybinding-matchers.ts +4 -0
- package/src/modes/utils/macos-option-forwarding.ts +80 -0
- package/src/progress/collect-project-progress.ts +153 -0
- package/src/progress/progress-contract.ts +172 -0
- package/src/progress/progress-snapshot.ts +140 -0
- package/src/progress/project-progress.ts +446 -0
- package/src/progress/render-progress.ts +209 -0
- package/src/prompts/system/master-mode.md +1 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/runtime-mcp/client.ts +4 -2
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/json-rpc.ts +2 -1
- package/src/runtime-mcp/manager.ts +29 -11
- package/src/runtime-mcp/mcp-redirect.ts +73 -0
- package/src/runtime-mcp/oauth-flow.ts +14 -2
- package/src/runtime-mcp/oauth-public-url.ts +14 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/pool.ts +6 -0
- package/src/runtime-mcp/project-host-values.ts +46 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +4 -3
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +95 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +81 -6
- package/src/sdk/broker/doctor-restart.ts +4 -0
- package/src/sdk/broker/ensure.ts +31 -2
- package/src/sdk/broker/lifecycle.ts +711 -66
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +238 -43
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2705 -303
- package/src/sdk/host/session-runtime.ts +490 -143
- package/src/sdk/host/surface-policy.ts +1 -0
- package/src/sdk/index.ts +8 -0
- package/src/sdk/lifecycle-session.ts +3 -1
- package/src/sdk/prompt-deadline-manager.ts +15 -0
- package/src/sdk/protocol/operation-inventory.generated.json +146 -0
- package/src/sdk/protocol/operation-registry.ts +9 -2
- package/src/sdk/router/session-router.ts +82 -18
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +69 -45
- package/src/session/agent-session.ts +679 -238
- package/src/session/artifacts.ts +176 -71
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3779 -462
- package/src/session/internal/managed-session-storage.ts +501 -78
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +19 -3
- package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
- package/src/session/session-manager.ts +951 -133
- package/src/session/session-retirement.ts +573 -14
- package/src/session/session-storage.ts +447 -15
- package/src/session/task-artifact-owner-codec.ts +71 -8
- package/src/session/task-artifact-owner-retirement.ts +21 -19
- package/src/setup/credential-auto-import.ts +12 -3
- package/src/setup/credential-import.ts +176 -5
- package/src/setup/paseo/announce.ts +6 -1
- package/src/skill-state/active-state.ts +145 -2
- package/src/skill-state/workflow-mutation-guard.ts +102 -10
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/mcp.ts +8 -4
- package/src/slash-commands/helpers/progress-report.ts +23 -0
- package/src/stt/setup.ts +3 -5
- package/src/task/commands.ts +3 -1
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +6 -0
- package/src/task/isolation-dir.ts +143 -0
- package/src/task/worktree.ts +16 -7
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit-write-path.ts +68 -0
- package/src/tools/ast-edit.ts +10 -1
- package/src/tools/bash-allowed-prefixes.ts +15 -0
- package/src/tools/bash-skill-urls.ts +56 -3
- package/src/tools/bash.ts +48 -79
- package/src/tools/browser/cdp-url.ts +18 -0
- package/src/tools/browser.ts +8 -1
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/gh.ts +10 -0
- package/src/tools/github-side-effect.ts +112 -0
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/monitor-mutation.ts +27 -0
- package/src/tools/monitor.ts +7 -0
- package/src/tools/python.ts +188 -66
- package/src/tools/recipe/index.ts +9 -0
- package/src/tools/recipe/recipe-guard.ts +19 -0
- package/src/tools/restricted-cwd.ts +10 -0
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +28 -1
- package/src/tools/todo-write.ts +10 -1
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/write.ts +1 -0
- package/src/utils/external-editor.ts +3 -2
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/insane/url-guard.ts +2 -1
- package/src/web/search/providers/codex.ts +3 -2
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import * as util from "node:util";
|
|
5
6
|
|
|
6
7
|
import type { RecoveryFsRoot } from "@gajae-code/natives";
|
|
7
8
|
|
|
@@ -12,6 +13,7 @@ type NativeManagedScope = Pick<
|
|
|
12
13
|
| "exactRestore"
|
|
13
14
|
| "exactUnlink"
|
|
14
15
|
| "snapshotDirectoryTree"
|
|
16
|
+
| "snapshotEmptyDirectory"
|
|
15
17
|
| "verifyOwnerOnlyPathSecurity"
|
|
16
18
|
| "verifyOwnerOnlyPathSecurityExpected"
|
|
17
19
|
>;
|
|
@@ -26,28 +28,53 @@ import {
|
|
|
26
28
|
FileSessionStorage,
|
|
27
29
|
type NativeDirectoryTreeSnapshot,
|
|
28
30
|
type SessionStorageFileIdentity,
|
|
31
|
+
type SessionStorageSnapshot,
|
|
29
32
|
type VerifiedSessionDeleteResult,
|
|
30
33
|
type VerifiedSessionDeleteTarget,
|
|
31
34
|
} from "../session-storage";
|
|
35
|
+
import { parseFirstJsonlLine } from "../session-transcript-header";
|
|
36
|
+
import {
|
|
37
|
+
parseTaskArtifactOwnerDeletionEvidence,
|
|
38
|
+
parseTaskArtifactOwnerRetirementOutcome,
|
|
39
|
+
type TaskArtifactOwnerDeletionEvidence,
|
|
40
|
+
type TaskArtifactOwnerStorageContext,
|
|
41
|
+
} from "../task-artifact-owner-codec";
|
|
42
|
+
import { verifyTaskArtifactOwnerPhysicalRetirement } from "../task-artifact-owner-retirement";
|
|
43
|
+
import {
|
|
44
|
+
continuationExtendsPrevious,
|
|
45
|
+
type ManagedGcSessionRetirementReceipt,
|
|
46
|
+
type ManagedGcSessionRetirementState,
|
|
47
|
+
type ManagedGcSessionRetirementTarget,
|
|
48
|
+
managedGcRetirementIdentityRecord,
|
|
49
|
+
managedGcRetirementIdentityValue,
|
|
50
|
+
parseManagedGcRetirementReceipt,
|
|
51
|
+
sameIdentity,
|
|
52
|
+
} from "./managed-gc-retirement-codec";
|
|
32
53
|
import {
|
|
33
54
|
acquireManagedLock,
|
|
34
55
|
assertManagedDirectoryRoot,
|
|
35
56
|
captureManagedFileNoFollow,
|
|
57
|
+
captureManagedFileNoFollowBounded,
|
|
36
58
|
captureManagedFilePrefixNoFollow,
|
|
37
59
|
copyManagedFileNoReplace,
|
|
38
60
|
ensureManagedDirectory,
|
|
39
61
|
fsyncManagedArtifactTree,
|
|
40
62
|
inspectManagedFileNoFollow,
|
|
63
|
+
isManagedLockQuarantineName,
|
|
41
64
|
MANAGED_ARTIFACT_COPY_BATCH_SIZE,
|
|
65
|
+
MANAGED_ARTIFACT_MAX_FILE_BYTES,
|
|
42
66
|
MANAGED_ARTIFACT_MAX_FILES,
|
|
43
67
|
MANAGED_ARTIFACT_MAX_TOTAL_BYTES,
|
|
68
|
+
MANAGED_SESSION_READ_RANGE_MAX_BYTES,
|
|
44
69
|
type ManagedDirectoryRoot,
|
|
45
70
|
type ManagedFileSnapshot,
|
|
46
71
|
ManagedPublishError,
|
|
47
72
|
ManagedSessionDescendantStore,
|
|
48
73
|
type ManagedSessionSecurityPolicy,
|
|
49
74
|
type ManagedStorageLock,
|
|
75
|
+
managedDirectoryRoot,
|
|
50
76
|
managedSecurityFailureClassification,
|
|
77
|
+
parseManagedLockRecord,
|
|
51
78
|
prepareManagedDirectoryRoot,
|
|
52
79
|
publishManagedFileNoReplace,
|
|
53
80
|
publishManagedTombstone,
|
|
@@ -56,6 +83,22 @@ import {
|
|
|
56
83
|
validateManagedArtifactTree,
|
|
57
84
|
validateNativeSecurityResult,
|
|
58
85
|
} from "./managed-session-storage";
|
|
86
|
+
import {
|
|
87
|
+
hasUnsupportedLegacyOwnerTarget as hasUnsupportedLegacyOwnerTargetInternal,
|
|
88
|
+
type ManagedGcOwnerCleanupAuthority,
|
|
89
|
+
type ManagedGcOwnerDeleteFields,
|
|
90
|
+
type ManagedGcOwnerProgress,
|
|
91
|
+
managedGcOwnerDeleteFields as managedGcOwnerDeleteFieldsInternal,
|
|
92
|
+
managedGcOwnerProgressBeforeDelete as managedGcOwnerProgressBeforeDeleteInternal,
|
|
93
|
+
persistManagedGcOwnerTranscriptDeletion,
|
|
94
|
+
persistManagedGcStorageOwnerDisposition as persistManagedGcStorageOwnerDispositionInternal,
|
|
95
|
+
prepareManagedGcOwnerTargets as prepareManagedGcOwnerTargetsInternal,
|
|
96
|
+
publishManagedGcArtifactsRemoved as publishManagedGcArtifactsRemovedInternal,
|
|
97
|
+
retireManagedGcOwnerAfterArtifacts as retireManagedGcOwnerAfterArtifactsInternal,
|
|
98
|
+
taskArtifactOwnerTranscriptResult as taskArtifactOwnerTranscriptResultInternal,
|
|
99
|
+
} from "./managed-task-owner-cleanup";
|
|
100
|
+
import { captureTaskArtifactOwnerDeletionEvidence } from "./task-artifact-owner-access";
|
|
101
|
+
import { taskArtifactOwnerLocatorFromTranscriptBytes } from "./task-artifact-owner-transcript";
|
|
59
102
|
|
|
60
103
|
export const MANAGED_SESSION_LAYOUT_VERSION = 2 as const;
|
|
61
104
|
export const MANAGED_SESSION_IDENTITY_VERSION = 1 as const;
|
|
@@ -74,6 +117,66 @@ export interface ManagedScope {
|
|
|
74
117
|
platform: "posix" | "win32";
|
|
75
118
|
}
|
|
76
119
|
|
|
120
|
+
export interface ManagedGcProtocolPathIdentity {
|
|
121
|
+
readonly path: string;
|
|
122
|
+
readonly dev: string;
|
|
123
|
+
readonly ino: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ManagedGcProtocolFileSnapshot {
|
|
127
|
+
readonly name: string;
|
|
128
|
+
readonly dev: string;
|
|
129
|
+
readonly ino: string;
|
|
130
|
+
readonly nlink: string;
|
|
131
|
+
readonly size: number;
|
|
132
|
+
readonly mtimeNs: string;
|
|
133
|
+
readonly ctimeNs: string;
|
|
134
|
+
readonly mode: number;
|
|
135
|
+
readonly sha256: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type ManagedGcProtocolRole = "locks" | "receipts" | "tombstones";
|
|
139
|
+
|
|
140
|
+
export interface ManagedGcProtocolDirectorySnapshot {
|
|
141
|
+
readonly role: ManagedGcProtocolRole;
|
|
142
|
+
readonly path: string;
|
|
143
|
+
readonly dev: string;
|
|
144
|
+
readonly ino: string;
|
|
145
|
+
readonly mtimeNs: string;
|
|
146
|
+
readonly ctimeNs: string;
|
|
147
|
+
readonly mode: number;
|
|
148
|
+
readonly files: readonly ManagedGcProtocolFileSnapshot[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Read-only protocol data; never a deletion capability. */
|
|
152
|
+
export interface ManagedGcProtocolScopeSnapshot {
|
|
153
|
+
readonly configuredRoot: ManagedGcProtocolPathIdentity;
|
|
154
|
+
readonly profile: ManagedGcProtocolPathIdentity;
|
|
155
|
+
readonly sessionsRoot: ManagedGcProtocolPathIdentity;
|
|
156
|
+
readonly scope: ManagedGcProtocolPathIdentity;
|
|
157
|
+
readonly binding: ManagedGcProtocolFileSnapshot;
|
|
158
|
+
readonly protocol: ManagedGcProtocolPathIdentity;
|
|
159
|
+
readonly protocolMtimeNs: string;
|
|
160
|
+
readonly protocolCtimeNs: string;
|
|
161
|
+
readonly protocolMode: number;
|
|
162
|
+
readonly directories: readonly ManagedGcProtocolDirectorySnapshot[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ManagedGcProtocolScopeInput {
|
|
166
|
+
readonly scopePath: string;
|
|
167
|
+
readonly scopeIdentity: ManagedGcProtocolPathIdentity;
|
|
168
|
+
readonly bindingIdentity: ManagedGcProtocolFileSnapshot;
|
|
169
|
+
readonly protocolIdentity: ManagedGcProtocolScopeSnapshot["protocol"] & {
|
|
170
|
+
readonly mtimeNs: string;
|
|
171
|
+
readonly ctimeNs: string;
|
|
172
|
+
readonly mode: number;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type ManagedGcProtocolScopeInspector = (
|
|
177
|
+
inputs: readonly ManagedGcProtocolScopeInput[],
|
|
178
|
+
) => Promise<readonly ManagedGcProtocolScopeSnapshot[]>;
|
|
179
|
+
|
|
77
180
|
/**
|
|
78
181
|
* Opaque managed writer authority captured by a trusted destination. The open
|
|
79
182
|
* transaction must use this authority rather than reacquiring its root from a
|
|
@@ -86,9 +189,23 @@ export interface ManagedCandidateWriteAuthority {
|
|
|
86
189
|
}
|
|
87
190
|
|
|
88
191
|
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof prepareManagedDirectoryRoot>>();
|
|
192
|
+
const managedScopeConfigurations = new WeakMap<
|
|
193
|
+
ManagedScope,
|
|
194
|
+
Readonly<
|
|
195
|
+
Pick<ManagedScope, "agentDir" | "sessionsRoot" | "canonicalCwd" | "directoryName" | "directoryPath" | "platform">
|
|
196
|
+
>
|
|
197
|
+
>();
|
|
89
198
|
const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
|
|
90
199
|
const managedDirectoryAuthorities = new WeakMap<ManagedScope, RecoveryFsRoot | undefined>();
|
|
91
200
|
const boundManagedWriteAuthorities = new WeakMap<ManagedScope, ManagedCandidateWriteAuthority>();
|
|
201
|
+
const readManagedGcScopeIdentities = new WeakMap<
|
|
202
|
+
ManagedScope,
|
|
203
|
+
{
|
|
204
|
+
readonly configuredRoot: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
|
|
205
|
+
readonly profile: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
|
|
206
|
+
readonly sessions: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
|
|
207
|
+
}
|
|
208
|
+
>();
|
|
92
209
|
|
|
93
210
|
function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandidateWriteAuthority): void {
|
|
94
211
|
if (
|
|
@@ -179,6 +296,7 @@ export type ManagedScopeErrorCode =
|
|
|
179
296
|
| "binding_conflict"
|
|
180
297
|
| "binding_invalid"
|
|
181
298
|
| "migration_busy"
|
|
299
|
+
| "managed_gc_journal_capacity_exceeded"
|
|
182
300
|
| "atomic_unavailable"
|
|
183
301
|
| "invalid_request"
|
|
184
302
|
| "durability_failed"
|
|
@@ -227,9 +345,14 @@ const managedScopeFailureCodes = new Set<ManagedScopeErrorCode>([
|
|
|
227
345
|
"durability_failed",
|
|
228
346
|
"durability_not_provable",
|
|
229
347
|
"migration_busy",
|
|
348
|
+
"managed_gc_journal_capacity_exceeded",
|
|
230
349
|
"capacity_exceeded",
|
|
231
350
|
]);
|
|
232
351
|
|
|
352
|
+
function isManagedGcJournalCapacityError(error: unknown): boolean {
|
|
353
|
+
return error instanceof Error && error.message === "managed_gc_journal_capacity_exceeded";
|
|
354
|
+
}
|
|
355
|
+
|
|
233
356
|
/**
|
|
234
357
|
* A scope whose directory outgrew a scan budget. The binding is untouched and
|
|
235
358
|
* canonical; only the surrounding entry count is over budget, so this must not
|
|
@@ -315,6 +438,7 @@ export type ManagedOpenFailure =
|
|
|
315
438
|
| "source_changed"
|
|
316
439
|
| "unsafe_artifacts"
|
|
317
440
|
| "artifact_capacity_exceeded"
|
|
441
|
+
| "managed_gc_journal_capacity_exceeded"
|
|
318
442
|
| "durability_failed"
|
|
319
443
|
| "atomic_unavailable"
|
|
320
444
|
| "invalid_request"
|
|
@@ -355,6 +479,8 @@ async function deleteSessionVerifiedWithFence(
|
|
|
355
479
|
stage: ManagedVerifiedDeleteTestEvent["stage"],
|
|
356
480
|
lock: ManagedStorageLock,
|
|
357
481
|
target: VerifiedSessionDeleteTarget,
|
|
482
|
+
scope: ManagedScope,
|
|
483
|
+
retiredTarget: RetiredTarget,
|
|
358
484
|
verifyAuthority?: () => void,
|
|
359
485
|
): Promise<VerifiedSessionDeleteResult> {
|
|
360
486
|
lock.assertOwned();
|
|
@@ -363,7 +489,14 @@ async function deleteSessionVerifiedWithFence(
|
|
|
363
489
|
if (hook) await hook({ flow, stage });
|
|
364
490
|
lock.assertOwned();
|
|
365
491
|
verifyAuthority?.();
|
|
366
|
-
|
|
492
|
+
const ownerFields = await managedGcOwnerDeleteFields(scope, retiredTarget, lock);
|
|
493
|
+
assertOwnerTranscriptLocator(target, ownerFields.taskArtifactOwnerDeletionEvidence);
|
|
494
|
+
lock.assertOwned();
|
|
495
|
+
verifyAuthority?.();
|
|
496
|
+
return new FileSessionStorage().deleteSessionVerified(
|
|
497
|
+
{ ...target, ...ownerFields },
|
|
498
|
+
managedGcProtocolInspectorForLock(scope, lock),
|
|
499
|
+
);
|
|
367
500
|
}
|
|
368
501
|
|
|
369
502
|
type NativeIdentity =
|
|
@@ -615,6 +748,17 @@ function resolveManagedScopeInternal(
|
|
|
615
748
|
directoryPath: path.join(sessionsRoot, `v2-${digest}`),
|
|
616
749
|
platform: identity.platform,
|
|
617
750
|
};
|
|
751
|
+
managedScopeConfigurations.set(
|
|
752
|
+
scope,
|
|
753
|
+
Object.freeze({
|
|
754
|
+
agentDir: scope.agentDir,
|
|
755
|
+
sessionsRoot: scope.sessionsRoot,
|
|
756
|
+
canonicalCwd: scope.canonicalCwd,
|
|
757
|
+
directoryName: scope.directoryName,
|
|
758
|
+
directoryPath: scope.directoryPath,
|
|
759
|
+
platform: scope.platform,
|
|
760
|
+
}),
|
|
761
|
+
);
|
|
618
762
|
try {
|
|
619
763
|
const root = fs.lstatSync(sessionsRoot);
|
|
620
764
|
if (!root.isDirectory() || root.isSymbolicLink()) {
|
|
@@ -649,7 +793,10 @@ function resolveManagedScopeInternal(
|
|
|
649
793
|
"verify",
|
|
650
794
|
"directory",
|
|
651
795
|
);
|
|
652
|
-
if (
|
|
796
|
+
if (
|
|
797
|
+
!security.ok &&
|
|
798
|
+
(!allowRepairableAclFailure || (security.code !== "acl_verify_failed" && security.code !== "owner_mismatch"))
|
|
799
|
+
) {
|
|
653
800
|
return {
|
|
654
801
|
kind: "error",
|
|
655
802
|
code: "binding_invalid",
|
|
@@ -677,6 +824,71 @@ export function resolveManagedScopeForWrite(input: ManagedScopeInput): ManagedSc
|
|
|
677
824
|
return resolveManagedScopeInternal(input, true);
|
|
678
825
|
}
|
|
679
826
|
|
|
827
|
+
/** Resolve only an existing, authenticated v2 scope; this path never initializes or repairs storage. */
|
|
828
|
+
export function resolveManagedGcScopeForRead(input: ManagedScopeInput): ManagedScopeResolution {
|
|
829
|
+
const resolved = resolveManagedScope(input);
|
|
830
|
+
if (resolved.kind === "error") return resolved;
|
|
831
|
+
const scope = resolved.scope;
|
|
832
|
+
try {
|
|
833
|
+
const rootPath = configuredRootPath(scope);
|
|
834
|
+
if (
|
|
835
|
+
path.resolve(scope.agentDir) !== scope.agentDir ||
|
|
836
|
+
path.resolve(scope.sessionsRoot) !== scope.sessionsRoot ||
|
|
837
|
+
path.dirname(scope.directoryPath) !== scope.sessionsRoot ||
|
|
838
|
+
scope.directoryName !== `v2-${scopeDigest(scope.platform, scope.canonicalCwd)}`
|
|
839
|
+
)
|
|
840
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
841
|
+
const captureDirectory = (pathname: string) => {
|
|
842
|
+
const stat = fs.lstatSync(pathname, { bigint: true });
|
|
843
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
844
|
+
const security = validateNativeSecurityResult(
|
|
845
|
+
verifyExistingManagedScopeDirectory(pathname),
|
|
846
|
+
"verify",
|
|
847
|
+
"directory",
|
|
848
|
+
);
|
|
849
|
+
if (!security.ok) throw new Error("managed_gc_scope_authority_mismatch");
|
|
850
|
+
return { path: pathname, dev: stat.dev, ino: stat.ino };
|
|
851
|
+
};
|
|
852
|
+
const configuredRoot = captureDirectory(rootPath);
|
|
853
|
+
const profile = captureDirectory(scope.agentDir);
|
|
854
|
+
const sessions = captureDirectory(scope.sessionsRoot);
|
|
855
|
+
const scopeDirectory = captureDirectory(scope.directoryPath);
|
|
856
|
+
const rootAuthority = managedDirectoryRoot(rootPath);
|
|
857
|
+
if (
|
|
858
|
+
rootAuthority.canonicalPath !== rootPath ||
|
|
859
|
+
rootAuthority.dev !== BigInt.asUintN(64, configuredRoot.dev) ||
|
|
860
|
+
rootAuthority.ino !== BigInt.asUintN(64, configuredRoot.ino)
|
|
861
|
+
)
|
|
862
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
863
|
+
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
864
|
+
const binding = captureManagedFileNoFollow(bindingPath);
|
|
865
|
+
if (validateBindingRaw(scope, binding.bytes.toString("utf8")))
|
|
866
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
867
|
+
for (const expected of [configuredRoot, profile, sessions, scopeDirectory]) {
|
|
868
|
+
const current = fs.lstatSync(expected.path, { bigint: true });
|
|
869
|
+
if (
|
|
870
|
+
!current.isDirectory() ||
|
|
871
|
+
current.isSymbolicLink() ||
|
|
872
|
+
current.dev !== expected.dev ||
|
|
873
|
+
current.ino !== expected.ino
|
|
874
|
+
)
|
|
875
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
876
|
+
}
|
|
877
|
+
managedRoots.set(scope, rootAuthority);
|
|
878
|
+
managedDirectoryIdentities.set(scope, { dev: scopeDirectory.dev, ino: scopeDirectory.ino });
|
|
879
|
+
managedDirectoryAuthorities.set(scope, undefined);
|
|
880
|
+
readManagedGcScopeIdentities.set(scope, { configuredRoot, profile, sessions });
|
|
881
|
+
return resolved;
|
|
882
|
+
} catch (error) {
|
|
883
|
+
return {
|
|
884
|
+
kind: "error",
|
|
885
|
+
code: "binding_invalid",
|
|
886
|
+
message: "The existing managed GC read authority could not be verified.",
|
|
887
|
+
cause: { classification: managedScopeResidualClassification(error) ?? "managed_gc_scope_authority_mismatch" },
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
680
892
|
function legacyDirectoryNames(
|
|
681
893
|
platform: ManagedScope["platform"],
|
|
682
894
|
canonicalCwd: string,
|
|
@@ -1015,6 +1227,17 @@ export async function ensureManagedScope(
|
|
|
1015
1227
|
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
1016
1228
|
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink()) throw new Error("reparse_point");
|
|
1017
1229
|
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
1230
|
+
if (!managedDirectoryAuthorities.has(scope)) {
|
|
1231
|
+
const authority = boundManagedWriteAuthorities.get(scope);
|
|
1232
|
+
const retainedAuthority =
|
|
1233
|
+
authority?.retainedAuthority &&
|
|
1234
|
+
authority.retainedDirectory !== undefined &&
|
|
1235
|
+
path.resolve(authority.retainedDirectory) === path.resolve(scope.directoryPath)
|
|
1236
|
+
? authority.retainedAuthority
|
|
1237
|
+
: retainManagedDirectoryAuthority(root, scope.directoryPath, managedDirectoryIdentityForScope(scope));
|
|
1238
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
1239
|
+
managedDirectoryAuthorities.set(scope, retainedAuthority);
|
|
1240
|
+
}
|
|
1018
1241
|
return { kind: "resolved", scope };
|
|
1019
1242
|
} catch (error) {
|
|
1020
1243
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
@@ -1815,6 +2038,7 @@ function expectedFailure(error: unknown): ManagedOpenFailure {
|
|
|
1815
2038
|
message === "source_changed" ||
|
|
1816
2039
|
message === "unsafe_artifacts" ||
|
|
1817
2040
|
message === "artifact_capacity_exceeded" ||
|
|
2041
|
+
message === "managed_gc_journal_capacity_exceeded" ||
|
|
1818
2042
|
message === "durability_failed" ||
|
|
1819
2043
|
message === "atomic_unavailable" ||
|
|
1820
2044
|
message === "invalid_request" ||
|
|
@@ -2076,235 +2300,2829 @@ function preparedReceiptMatches(
|
|
|
2076
2300
|
}
|
|
2077
2301
|
}
|
|
2078
2302
|
|
|
2079
|
-
type RetiredTarget = ManagedCandidate
|
|
2303
|
+
type RetiredTarget = ManagedCandidate & {
|
|
2304
|
+
readonly taskArtifactOwnerDeletionEvidence?: TaskArtifactOwnerDeletionEvidence;
|
|
2305
|
+
};
|
|
2080
2306
|
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2307
|
+
const MANAGED_GC_RETIREMENT_PREFIX = "gc-retirement-";
|
|
2308
|
+
const MANAGED_GC_RETIREMENT_RECEIPTS = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_RECEIPTS_DIRECTORY}`;
|
|
2309
|
+
const MANAGED_GC_RETIREMENT_LOCKS = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_LOCKS_DIRECTORY}`;
|
|
2310
|
+
// History capacity is shared by every transcript in one scope, so publishers must share one lease.
|
|
2311
|
+
const MANAGED_GC_RETIREMENT_SCOPE_LOCK_NAME = `${MANAGED_GC_RETIREMENT_PREFIX}${createHash("sha256")
|
|
2312
|
+
.update("managed-gc-retirement-scope-lock-v1", "utf8")
|
|
2313
|
+
.digest("hex")}`;
|
|
2314
|
+
interface ManagedGcTrustedScope {
|
|
2315
|
+
readonly root: ManagedDirectoryRoot;
|
|
2316
|
+
readonly retainedAuthority: RecoveryFsRoot | undefined;
|
|
2317
|
+
readonly identity: { readonly dev: bigint; readonly ino: bigint };
|
|
2318
|
+
readonly policy: ManagedSessionSecurityPolicy;
|
|
2319
|
+
readonly ownerContext: TaskArtifactOwnerStorageContext;
|
|
2320
|
+
}
|
|
2321
|
+
function managedGcTrustedScope(scope: ManagedScope): ManagedGcTrustedScope {
|
|
2322
|
+
const root = managedRoots.get(scope);
|
|
2323
|
+
const identity = managedDirectoryIdentities.get(scope);
|
|
2324
|
+
const configuration = managedScopeConfigurations.get(scope);
|
|
2325
|
+
if (!root || !identity || !configuration || !managedDirectoryAuthorities.has(scope))
|
|
2326
|
+
throw new Error("managed_gc_scope_authority_unavailable");
|
|
2327
|
+
if (
|
|
2328
|
+
scope.agentDir !== configuration.agentDir ||
|
|
2329
|
+
scope.sessionsRoot !== configuration.sessionsRoot ||
|
|
2330
|
+
scope.canonicalCwd !== configuration.canonicalCwd ||
|
|
2331
|
+
scope.directoryName !== configuration.directoryName ||
|
|
2332
|
+
scope.directoryPath !== configuration.directoryPath ||
|
|
2333
|
+
scope.platform !== configuration.platform ||
|
|
2334
|
+
scope.apiVersion !== 1 ||
|
|
2335
|
+
scope.layoutVersion !== MANAGED_SESSION_LAYOUT_VERSION ||
|
|
2336
|
+
scope.identityVersion !== MANAGED_SESSION_IDENTITY_VERSION
|
|
2337
|
+
)
|
|
2338
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
2339
|
+
const policy: ManagedSessionSecurityPolicy =
|
|
2340
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default";
|
|
2341
|
+
assertManagedDirectoryRoot(root);
|
|
2342
|
+
managedDirectoryIdentityForScope(scope);
|
|
2343
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
2344
|
+
const readIdentities = readManagedGcScopeIdentities.get(scope);
|
|
2345
|
+
if (readIdentities) {
|
|
2346
|
+
for (const expected of [readIdentities.configuredRoot, readIdentities.profile, readIdentities.sessions]) {
|
|
2347
|
+
const current = fs.lstatSync(expected.path, { bigint: true });
|
|
2100
2348
|
if (
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
(item.provenance !== undefined && item.provenance !== "v2" && item.provenance !== "legacy") ||
|
|
2106
|
-
typeof fields.canonicalPath !== "string" ||
|
|
2107
|
-
typeof fields.dev !== "string" ||
|
|
2108
|
-
typeof fields.ino !== "string" ||
|
|
2109
|
-
typeof fields.size !== "number" ||
|
|
2110
|
-
typeof fields.mtimeMs !== "number" ||
|
|
2111
|
-
typeof fields.mtimeNs !== "string" ||
|
|
2112
|
-
typeof fields.sha256 !== "string"
|
|
2349
|
+
!current.isDirectory() ||
|
|
2350
|
+
current.isSymbolicLink() ||
|
|
2351
|
+
current.dev !== expected.dev ||
|
|
2352
|
+
current.ino !== expected.ino
|
|
2113
2353
|
)
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
identity: {
|
|
2128
|
-
canonicalPath: fields.canonicalPath,
|
|
2129
|
-
dev: BigInt(fields.dev),
|
|
2130
|
-
ino: BigInt(fields.ino),
|
|
2131
|
-
size: fields.size,
|
|
2132
|
-
mtimeMs: fields.mtimeMs,
|
|
2133
|
-
mtimeNs: BigInt(fields.mtimeNs),
|
|
2134
|
-
sha256: fields.sha256,
|
|
2135
|
-
sessionId: item.sessionId,
|
|
2136
|
-
},
|
|
2137
|
-
});
|
|
2354
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
2355
|
+
}
|
|
2356
|
+
for (const pathname of [
|
|
2357
|
+
readIdentities.configuredRoot.path,
|
|
2358
|
+
readIdentities.profile.path,
|
|
2359
|
+
readIdentities.sessions.path,
|
|
2360
|
+
]) {
|
|
2361
|
+
const verified = validateNativeSecurityResult(
|
|
2362
|
+
verifyExistingManagedScopeDirectory(pathname),
|
|
2363
|
+
"verify",
|
|
2364
|
+
"directory",
|
|
2365
|
+
);
|
|
2366
|
+
if (!verified.ok) throw new Error("managed_gc_scope_authority_mismatch");
|
|
2138
2367
|
}
|
|
2139
|
-
return targets;
|
|
2140
|
-
} catch {
|
|
2141
|
-
return undefined;
|
|
2142
2368
|
}
|
|
2369
|
+
return {
|
|
2370
|
+
root,
|
|
2371
|
+
retainedAuthority: managedDirectoryAuthorities.get(scope),
|
|
2372
|
+
identity,
|
|
2373
|
+
policy,
|
|
2374
|
+
ownerContext: Object.freeze({
|
|
2375
|
+
rootAuthority: root,
|
|
2376
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2377
|
+
securityPolicy: policy,
|
|
2378
|
+
profileAgentDir: scope.agentDir,
|
|
2379
|
+
}),
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
/** Build owner-storage authority only from a previously prepared trusted managed scope. */
|
|
2383
|
+
export function taskArtifactOwnerStorageContextForScope(scope: ManagedScope): TaskArtifactOwnerStorageContext {
|
|
2384
|
+
return managedGcTrustedScope(scope).ownerContext;
|
|
2143
2385
|
}
|
|
2144
2386
|
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2387
|
+
function managedGcOwnerCleanupAuthority(scope: ManagedScope, lock: ManagedStorageLock): ManagedGcOwnerCleanupAuthority {
|
|
2388
|
+
return {
|
|
2389
|
+
agentDir: scope.agentDir,
|
|
2390
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2391
|
+
directoryPath: scope.directoryPath,
|
|
2392
|
+
storageContext: taskArtifactOwnerStorageContextForScope(scope),
|
|
2393
|
+
inspectProtocol: managedGcProtocolInspectorForLock(scope, lock),
|
|
2394
|
+
assertOwned: () => lock.assertOwned(),
|
|
2395
|
+
bindTarget: transcriptPath => bindManagedGcSessionRetirementTarget(scope, transcriptPath),
|
|
2396
|
+
readReceipt: async transcriptPath => {
|
|
2397
|
+
lock.assertOwned();
|
|
2398
|
+
const receipt = await readManagedGcSessionRetirementReceipt(scope, transcriptPath);
|
|
2399
|
+
lock.assertOwned();
|
|
2400
|
+
return receipt;
|
|
2401
|
+
},
|
|
2402
|
+
publishReceipt: async receipt => {
|
|
2403
|
+
lock.assertOwned();
|
|
2404
|
+
const published = await publishManagedGcSessionRetirementReceipt(scope, receipt);
|
|
2405
|
+
lock.assertOwned();
|
|
2406
|
+
return published;
|
|
2407
|
+
},
|
|
2408
|
+
findCompletedRetirement: async (evidence, exceptTranscriptPath) => {
|
|
2409
|
+
lock.assertOwned();
|
|
2410
|
+
const receipts = await discoverManagedGcSessionRetirementReceipts({
|
|
2411
|
+
agentDir: scope.agentDir,
|
|
2412
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2413
|
+
});
|
|
2414
|
+
lock.assertOwned();
|
|
2415
|
+
const match = receipts.find(
|
|
2416
|
+
({ scope: source, receipt }) =>
|
|
2417
|
+
source.agentDir === scope.agentDir &&
|
|
2418
|
+
source.sessionsRoot === scope.sessionsRoot &&
|
|
2419
|
+
source.directoryPath !== scope.directoryPath &&
|
|
2420
|
+
receipt.transcriptPath !== exceptTranscriptPath &&
|
|
2421
|
+
receipt.state === "owner_retired" &&
|
|
2422
|
+
receipt.taskArtifactOwnerRetirementOutcome?.kind === "completed" &&
|
|
2423
|
+
deepSame(receipt.taskArtifactOwnerDeletionEvidence, evidence),
|
|
2424
|
+
);
|
|
2425
|
+
return match
|
|
2426
|
+
? {
|
|
2427
|
+
scope: {
|
|
2428
|
+
agentDir: match.scope.agentDir,
|
|
2429
|
+
sessionsRoot: match.scope.sessionsRoot,
|
|
2430
|
+
directoryPath: match.scope.directoryPath,
|
|
2431
|
+
},
|
|
2432
|
+
receipt: match.receipt,
|
|
2433
|
+
}
|
|
2434
|
+
: undefined;
|
|
2435
|
+
},
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2156
2438
|
|
|
2157
|
-
function
|
|
2158
|
-
|
|
2159
|
-
if (!fs.existsSync(pathname)) return undefined;
|
|
2160
|
-
if (!isQuarantinePath(receipt.target, pathname) || !receipt.expectedArtifactsIdentity)
|
|
2161
|
-
throw new Error("durability_failed");
|
|
2162
|
-
const identity = artifactIdentityAt(pathname);
|
|
2163
|
-
if (
|
|
2164
|
-
!identity ||
|
|
2165
|
-
identity.dev !== receipt.expectedArtifactsIdentity.dev ||
|
|
2166
|
-
identity.ino !== receipt.expectedArtifactsIdentity.ino
|
|
2167
|
-
)
|
|
2168
|
-
throw new Error("durability_failed");
|
|
2169
|
-
const tree = snapshotArtifactTree(pathname);
|
|
2170
|
-
if (!artifactTreePayloadAbsent(tree)) throw new Error("durability_failed");
|
|
2171
|
-
return { path: pathname, identity, tree };
|
|
2439
|
+
function hasUnsupportedLegacyOwnerTarget(scope: ManagedScope, target: RetiredTarget): boolean {
|
|
2440
|
+
return hasUnsupportedLegacyOwnerTargetInternal(scope.directoryPath, target);
|
|
2172
2441
|
}
|
|
2173
2442
|
|
|
2174
|
-
function
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
const tree = artifactTreeSnapshot(retained.tree);
|
|
2181
|
-
const expectedRetainedPath =
|
|
2182
|
-
typeof record.detachedArtifactsPath === "string"
|
|
2183
|
-
? record.detachedArtifactsPath
|
|
2184
|
-
: typeof record.plannedArtifactsPath === "string"
|
|
2185
|
-
? deterministicRemovalRoot(record.plannedArtifactsPath)
|
|
2186
|
-
: undefined;
|
|
2187
|
-
if (
|
|
2188
|
-
!expectedRetainedPath ||
|
|
2189
|
-
retained.path !== expectedRetainedPath ||
|
|
2190
|
-
record.artifactsPayloadDurable !== true ||
|
|
2191
|
-
!identity ||
|
|
2192
|
-
typeof identity !== "object" ||
|
|
2193
|
-
Array.isArray(identity) ||
|
|
2194
|
-
!tree ||
|
|
2195
|
-
!artifactTreePayloadAbsent(tree)
|
|
2196
|
-
)
|
|
2197
|
-
throw new Error("durability_failed");
|
|
2198
|
-
const artifactIdentity = identity as Record<string, unknown>;
|
|
2199
|
-
if (
|
|
2200
|
-
typeof artifactIdentity.dev !== "string" ||
|
|
2201
|
-
typeof artifactIdentity.ino !== "string" ||
|
|
2202
|
-
typeof artifactIdentity.size !== "number" ||
|
|
2203
|
-
typeof artifactIdentity.mtimeNs !== "string" ||
|
|
2204
|
-
typeof artifactIdentity.sha256 !== "string"
|
|
2205
|
-
)
|
|
2206
|
-
throw new Error("durability_failed");
|
|
2207
|
-
if (!fs.existsSync(retained.path)) return true;
|
|
2208
|
-
const observed = artifactIdentityAt(retained.path);
|
|
2209
|
-
if (
|
|
2210
|
-
!observed ||
|
|
2211
|
-
observed.dev !== BigInt(artifactIdentity.dev) ||
|
|
2212
|
-
observed.ino !== BigInt(artifactIdentity.ino) ||
|
|
2213
|
-
!artifactTreePayloadAbsent(snapshotArtifactTree(retained.path))
|
|
2214
|
-
)
|
|
2215
|
-
throw new Error("durability_failed");
|
|
2216
|
-
return true;
|
|
2443
|
+
async function prepareManagedGcOwnerTargets(
|
|
2444
|
+
scope: ManagedScope,
|
|
2445
|
+
targets: readonly RetiredTarget[],
|
|
2446
|
+
lock: ManagedStorageLock,
|
|
2447
|
+
): Promise<RetiredTarget[]> {
|
|
2448
|
+
return prepareManagedGcOwnerTargetsInternal(managedGcOwnerCleanupAuthority(scope, lock), targets);
|
|
2217
2449
|
}
|
|
2218
2450
|
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
target: RetiredTarget
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
detachedArtifactsPath?: string;
|
|
2227
|
-
detachedTranscriptPath?: string;
|
|
2228
|
-
transcriptPayloadDurable?: true;
|
|
2229
|
-
retainedArtifactsSuccessorPath?: string;
|
|
2230
|
-
retainedArtifactsPlaceholderPath?: string;
|
|
2231
|
-
retainedArtifactsUnknownPath?: string;
|
|
2232
|
-
retainedTranscriptSuccessorPath?: string;
|
|
2233
|
-
retainedTranscriptPlaceholderPath?: string;
|
|
2234
|
-
retainedTranscriptUnknownPath?: string;
|
|
2451
|
+
async function managedGcOwnerDeleteFields(
|
|
2452
|
+
scope: ManagedScope,
|
|
2453
|
+
target: RetiredTarget,
|
|
2454
|
+
lock: ManagedStorageLock,
|
|
2455
|
+
): Promise<ManagedGcOwnerDeleteFields> {
|
|
2456
|
+
return managedGcOwnerDeleteFieldsInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
|
|
2457
|
+
}
|
|
2235
2458
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2459
|
+
async function publishManagedGcArtifactsRemoved(
|
|
2460
|
+
scope: ManagedScope,
|
|
2461
|
+
target: RetiredTarget,
|
|
2462
|
+
lock: ManagedStorageLock,
|
|
2463
|
+
): Promise<ManagedGcSessionRetirementReceipt | undefined> {
|
|
2464
|
+
return publishManagedGcArtifactsRemovedInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
|
|
2465
|
+
}
|
|
2239
2466
|
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2467
|
+
async function retireManagedGcOwnerAfterArtifacts(
|
|
2468
|
+
scope: ManagedScope,
|
|
2242
2469
|
target: RetiredTarget,
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
):
|
|
2246
|
-
return
|
|
2247
|
-
|
|
2248
|
-
|
|
2470
|
+
tombstoneTargets: readonly RetiredTarget[],
|
|
2471
|
+
lock: ManagedStorageLock,
|
|
2472
|
+
): Promise<ManagedGcOwnerProgress> {
|
|
2473
|
+
return retireManagedGcOwnerAfterArtifactsInternal(
|
|
2474
|
+
managedGcOwnerCleanupAuthority(scope, lock),
|
|
2475
|
+
target,
|
|
2476
|
+
tombstoneTargets,
|
|
2249
2477
|
);
|
|
2250
2478
|
}
|
|
2251
2479
|
|
|
2252
|
-
function
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
identity: receipt.target.identity,
|
|
2264
|
-
},
|
|
2265
|
-
...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
|
|
2266
|
-
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
2267
|
-
...(receipt.artifactsPayloadDurable === true ? { artifactsPayloadDurable: true } : {}),
|
|
2268
|
-
...(receipt.artifactsRemovedAttempt !== undefined
|
|
2269
|
-
? { artifactsRemovedAttempt: receipt.artifactsRemovedAttempt }
|
|
2270
|
-
: {}),
|
|
2271
|
-
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
2272
|
-
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
2273
|
-
...(receipt.transcriptPayloadDurable === true ? { transcriptPayloadDurable: true } : {}),
|
|
2274
|
-
...(receipt.retainedArtifactsSuccessorPath
|
|
2275
|
-
? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
|
|
2276
|
-
: {}),
|
|
2277
|
-
...(receipt.retainedArtifactsPlaceholderPath
|
|
2278
|
-
? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
|
|
2279
|
-
: {}),
|
|
2280
|
-
...(receipt.retainedArtifactsUnknownPath
|
|
2281
|
-
? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
|
|
2282
|
-
: {}),
|
|
2283
|
-
...(receipt.retainedTranscriptSuccessorPath
|
|
2284
|
-
? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
|
|
2285
|
-
: {}),
|
|
2286
|
-
...(receipt.retainedTranscriptPlaceholderPath
|
|
2287
|
-
? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
|
|
2288
|
-
: {}),
|
|
2289
|
-
...(receipt.retainedTranscriptUnknownPath
|
|
2290
|
-
? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
|
|
2291
|
-
: {}),
|
|
2292
|
-
plannedArtifactsPath: receipt.plannedArtifactsPath,
|
|
2293
|
-
plannedTranscriptPath: receipt.plannedTranscriptPath,
|
|
2294
|
-
};
|
|
2480
|
+
async function persistManagedGcStorageOwnerDisposition(
|
|
2481
|
+
scope: ManagedScope,
|
|
2482
|
+
target: RetiredTarget,
|
|
2483
|
+
deletion: Extract<VerifiedSessionDeleteResult, { kind: "cleanup_pending"; phase: "task_artifact_owner" }>,
|
|
2484
|
+
lock: ManagedStorageLock,
|
|
2485
|
+
): Promise<ManagedGcOwnerProgress> {
|
|
2486
|
+
return persistManagedGcStorageOwnerDispositionInternal(
|
|
2487
|
+
managedGcOwnerCleanupAuthority(scope, lock),
|
|
2488
|
+
target,
|
|
2489
|
+
deletion,
|
|
2490
|
+
);
|
|
2295
2491
|
}
|
|
2296
2492
|
|
|
2297
|
-
|
|
2493
|
+
async function managedGcOwnerProgressBeforeDelete(
|
|
2494
|
+
scope: ManagedScope,
|
|
2495
|
+
target: RetiredTarget,
|
|
2496
|
+
tombstoneTargets: readonly RetiredTarget[],
|
|
2497
|
+
artifactsRemoved: boolean,
|
|
2498
|
+
lock: ManagedStorageLock,
|
|
2499
|
+
): Promise<ManagedGcOwnerProgress> {
|
|
2500
|
+
return managedGcOwnerProgressBeforeDeleteInternal(
|
|
2501
|
+
managedGcOwnerCleanupAuthority(scope, lock),
|
|
2502
|
+
target,
|
|
2503
|
+
tombstoneTargets,
|
|
2504
|
+
artifactsRemoved,
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2298
2507
|
|
|
2299
|
-
function
|
|
2508
|
+
async function taskArtifactOwnerTranscriptResult(
|
|
2300
2509
|
scope: ManagedScope,
|
|
2301
|
-
tombstone: string,
|
|
2302
2510
|
target: RetiredTarget,
|
|
2303
|
-
|
|
2304
|
-
):
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2511
|
+
lock: ManagedStorageLock,
|
|
2512
|
+
): Promise<"none" | "retired" | "payload_retired"> {
|
|
2513
|
+
return taskArtifactOwnerTranscriptResultInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
|
|
2514
|
+
}
|
|
2515
|
+
function managedGcScopeStore(scope: ManagedScope, trusted: ManagedGcTrustedScope): ManagedSessionDescendantStore {
|
|
2516
|
+
return new ManagedSessionDescendantStore(
|
|
2517
|
+
trusted.root,
|
|
2518
|
+
scope.directoryPath,
|
|
2519
|
+
trusted.retainedAuthority
|
|
2520
|
+
? { authority: trusted.retainedAuthority, authorityBaseDir: scope.directoryPath }
|
|
2521
|
+
: undefined,
|
|
2522
|
+
trusted.policy,
|
|
2523
|
+
scope.agentDir,
|
|
2524
|
+
{
|
|
2525
|
+
canonicalPath: scope.directoryPath,
|
|
2526
|
+
dev: BigInt.asUintN(64, trusted.identity.dev),
|
|
2527
|
+
ino: BigInt.asUintN(64, trusted.identity.ino),
|
|
2528
|
+
},
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
function managedGcScopeReader(scope: ManagedScope, trusted: ManagedGcTrustedScope): ManagedSessionDescendantStore {
|
|
2532
|
+
return new ManagedSessionDescendantStore(
|
|
2533
|
+
trusted.root,
|
|
2534
|
+
scope.directoryPath,
|
|
2535
|
+
undefined,
|
|
2536
|
+
trusted.policy,
|
|
2537
|
+
scope.agentDir,
|
|
2538
|
+
{
|
|
2539
|
+
canonicalPath: scope.directoryPath,
|
|
2540
|
+
dev: BigInt.asUintN(64, trusted.identity.dev),
|
|
2541
|
+
ino: BigInt.asUintN(64, trusted.identity.ino),
|
|
2542
|
+
},
|
|
2543
|
+
"read-only",
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function managedGcWithClosedReader<T>(store: ManagedSessionDescendantStore, operation: () => T): T {
|
|
2548
|
+
let result: T | undefined;
|
|
2549
|
+
let operationError: unknown;
|
|
2550
|
+
let hasOperationError = false;
|
|
2551
|
+
try {
|
|
2552
|
+
result = operation();
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
operationError = error;
|
|
2555
|
+
hasOperationError = true;
|
|
2556
|
+
}
|
|
2557
|
+
let closeError: unknown;
|
|
2558
|
+
let hasCloseError = false;
|
|
2559
|
+
try {
|
|
2560
|
+
store.close();
|
|
2561
|
+
} catch (error) {
|
|
2562
|
+
closeError = error;
|
|
2563
|
+
hasCloseError = true;
|
|
2564
|
+
}
|
|
2565
|
+
if (hasOperationError) throw operationError;
|
|
2566
|
+
if (hasCloseError) throw closeError;
|
|
2567
|
+
return result as T;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function managedGcRetirementTranscriptKey(transcriptPath: string): string {
|
|
2571
|
+
return createHash("sha256").update(path.resolve(transcriptPath), "utf8").digest("hex");
|
|
2572
|
+
}
|
|
2573
|
+
function managedGcRetirementReceiptRelativePath(
|
|
2574
|
+
transcriptPath: string,
|
|
2575
|
+
state: ManagedGcSessionRetirementState,
|
|
2576
|
+
attempt?: number,
|
|
2577
|
+
): string {
|
|
2578
|
+
if (state === "owner_pending" && (!Number.isSafeInteger(attempt) || attempt === undefined || attempt < 1))
|
|
2579
|
+
throw new Error("task_artifact_owner_continuation_attempt_invalid");
|
|
2580
|
+
const suffix = state === "owner_pending" ? `${state}-${String(attempt).padStart(8, "0")}` : state;
|
|
2581
|
+
return `${MANAGED_GC_RETIREMENT_RECEIPTS}/${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-${suffix}.json`;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
const MANAGED_GC_RECEIPT_MAX_BYTES = MANAGED_SESSION_READ_RANGE_MAX_BYTES;
|
|
2585
|
+
|
|
2586
|
+
interface ManagedGcDirectoryEvidence {
|
|
2587
|
+
readonly dev: string;
|
|
2588
|
+
readonly ino: string;
|
|
2589
|
+
readonly mtimeNs: string;
|
|
2590
|
+
readonly ctimeNs: string;
|
|
2591
|
+
readonly mode: string;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
interface ManagedGcDirectoryInventory {
|
|
2595
|
+
readonly evidence: ManagedGcDirectoryEvidence;
|
|
2596
|
+
readonly entryCount: number;
|
|
2597
|
+
readonly physicalEntryCount: number;
|
|
2598
|
+
readonly entries: readonly fs.Dirent[];
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
const MANAGED_GC_INVENTORY_MAX_WORK_UNITS = MANAGED_ARTIFACT_MAX_FILES * 2;
|
|
2602
|
+
|
|
2603
|
+
type ManagedGcInventoryOperation =
|
|
2604
|
+
| { readonly kind: "inspection"; workUnits: number; observedEntries: number }
|
|
2605
|
+
| { readonly kind: "leased-journal"; readonly lock: ManagedStorageLock };
|
|
2606
|
+
|
|
2607
|
+
function newManagedGcInspectionOperation(): ManagedGcInventoryOperation {
|
|
2608
|
+
return { kind: "inspection", workUnits: 0, observedEntries: 0 };
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
function managedGcReserveInventoryWork(operation: ManagedGcInventoryOperation, amount = 1): void {
|
|
2612
|
+
if (!Number.isSafeInteger(amount) || amount < 0) managedGcJournalCapacity();
|
|
2613
|
+
if (operation.kind === "leased-journal") {
|
|
2614
|
+
operation.lock.assertOwned();
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (!Number.isSafeInteger(operation.workUnits) || operation.workUnits > MANAGED_GC_INVENTORY_MAX_WORK_UNITS - amount)
|
|
2618
|
+
managedGcJournalCapacity();
|
|
2619
|
+
operation.workUnits += amount;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
function managedGcComponentInputAllowance(pathname: string): number {
|
|
2623
|
+
if (typeof pathname !== "string") managedGcJournalCapacity();
|
|
2624
|
+
const allowance = pathname.length + 1;
|
|
2625
|
+
if (!Number.isSafeInteger(allowance)) managedGcJournalCapacity();
|
|
2626
|
+
return allowance;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
function managedGcReserveScopeComponentInputs(operation: ManagedGcInventoryOperation, scope: ManagedScope): void {
|
|
2630
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(scope.canonicalCwd));
|
|
2631
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(scope.agentDir));
|
|
2632
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(scope.sessionsRoot));
|
|
2633
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(scope.directoryPath));
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function managedGcAdmitInventoryEntry(operation: ManagedGcInventoryOperation): void {
|
|
2637
|
+
if (operation.kind === "inspection" && operation.observedEntries >= MANAGED_ARTIFACT_MAX_FILES)
|
|
2638
|
+
managedGcJournalCapacity();
|
|
2639
|
+
managedGcReserveInventoryWork(operation);
|
|
2640
|
+
if (operation.kind === "inspection") operation.observedEntries++;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
interface ManagedGcReceiptHistory {
|
|
2644
|
+
readonly entryCount: number;
|
|
2645
|
+
readonly encodedBytes: number;
|
|
2646
|
+
readonly directoryEntryCount: number;
|
|
2647
|
+
readonly directoryEvidence: ManagedGcDirectoryEvidence;
|
|
2648
|
+
readonly directoryInventory: ManagedGcDirectoryInventory;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
interface ManagedGcDiscoveryScopeFence {
|
|
2652
|
+
readonly scope: ManagedScope;
|
|
2653
|
+
readonly scopeIdentity: { readonly dev: string; readonly ino: string };
|
|
2654
|
+
readonly binding: ManagedFileSnapshot;
|
|
2655
|
+
readonly internalDirectoryIdentity: { readonly dev: string; readonly ino: string };
|
|
2656
|
+
readonly receiptInventory: ManagedGcDirectoryInventory | undefined;
|
|
2657
|
+
readonly openReader: () => ManagedSessionDescendantStore;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
interface MutableManagedGcReceiptHistory {
|
|
2661
|
+
encodedBytes: number;
|
|
2662
|
+
readonly admittedFiles: Set<string>;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
function managedGcJournalCapacity(): never {
|
|
2666
|
+
throw new Error("managed_gc_journal_capacity_exceeded");
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
function managedGcDirectoryEvidence(stat: fs.BigIntStats): ManagedGcDirectoryEvidence {
|
|
2670
|
+
return {
|
|
2671
|
+
dev: stat.dev.toString(),
|
|
2672
|
+
ino: stat.ino.toString(),
|
|
2673
|
+
mtimeNs: stat.mtimeNs.toString(),
|
|
2674
|
+
ctimeNs: stat.ctimeNs.toString(),
|
|
2675
|
+
mode: stat.mode.toString(),
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function managedGcSameDirectoryEvidence(left: ManagedGcDirectoryEvidence, right: ManagedGcDirectoryEvidence): boolean {
|
|
2680
|
+
return (
|
|
2681
|
+
left.dev === right.dev &&
|
|
2682
|
+
left.ino === right.ino &&
|
|
2683
|
+
left.mtimeNs === right.mtimeNs &&
|
|
2684
|
+
left.ctimeNs === right.ctimeNs &&
|
|
2685
|
+
left.mode === right.mode
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
/** Count the selected family before retaining a bounded inventory, preserving unrelated role entries. */
|
|
2690
|
+
function managedGcStreamDirectoryInventory(
|
|
2691
|
+
pathname: string,
|
|
2692
|
+
countEntry: (entry: fs.Dirent) => boolean,
|
|
2693
|
+
retain: (entry: fs.Dirent) => boolean,
|
|
2694
|
+
operation: ManagedGcInventoryOperation,
|
|
2695
|
+
): ManagedGcDirectoryInventory {
|
|
2696
|
+
managedGcReserveInventoryWork(operation);
|
|
2697
|
+
const beforeStat = fs.lstatSync(pathname, { bigint: true });
|
|
2698
|
+
if (!beforeStat.isDirectory() || beforeStat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
2699
|
+
const before = managedGcDirectoryEvidence(beforeStat);
|
|
2700
|
+
const scan = (collect: boolean): { count: number; physicalEntryCount: number; entries: fs.Dirent[] } => {
|
|
2701
|
+
managedGcReserveInventoryWork(operation);
|
|
2702
|
+
const entries: fs.Dirent[] = [];
|
|
2703
|
+
let count = 0;
|
|
2704
|
+
let physicalEntryCount = 0;
|
|
2705
|
+
let retained = 0;
|
|
2706
|
+
// Reserve open/close plus conservative processing for the dot entries Node suppresses.
|
|
2707
|
+
managedGcReserveInventoryWork(operation, 4);
|
|
2708
|
+
let directory: fs.Dir | undefined;
|
|
2709
|
+
let primaryError: unknown;
|
|
2710
|
+
let hasPrimaryError = false;
|
|
2711
|
+
try {
|
|
2712
|
+
directory = fs.opendirSync(pathname, { bufferSize: 1 });
|
|
2713
|
+
for (;;) {
|
|
2714
|
+
managedGcReserveInventoryWork(operation);
|
|
2715
|
+
const entry = directory.readSync();
|
|
2716
|
+
if (!entry) break;
|
|
2717
|
+
managedGcAdmitInventoryEntry(operation);
|
|
2718
|
+
if (!Number.isSafeInteger(physicalEntryCount + 1)) managedGcJournalCapacity();
|
|
2719
|
+
physicalEntryCount++;
|
|
2720
|
+
if (countEntry(entry) && ++count > MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
|
|
2721
|
+
if (!retain(entry)) continue;
|
|
2722
|
+
if (++retained > MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
|
|
2723
|
+
if (collect) {
|
|
2724
|
+
managedGcReserveInventoryWork(operation);
|
|
2725
|
+
entries.push(entry);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
} catch (error) {
|
|
2729
|
+
primaryError = error;
|
|
2730
|
+
hasPrimaryError = true;
|
|
2731
|
+
}
|
|
2732
|
+
let closeError: unknown;
|
|
2733
|
+
if (directory) {
|
|
2734
|
+
try {
|
|
2735
|
+
directory.closeSync();
|
|
2736
|
+
} catch (error) {
|
|
2737
|
+
closeError = error;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
if (hasPrimaryError) throw primaryError;
|
|
2741
|
+
if (closeError !== undefined) throw closeError;
|
|
2742
|
+
managedGcReserveInventoryWork(operation);
|
|
2743
|
+
return { count, physicalEntryCount, entries };
|
|
2744
|
+
};
|
|
2745
|
+
const counted = scan(false);
|
|
2746
|
+
managedGcReserveInventoryWork(operation);
|
|
2747
|
+
const afterCount = fs.lstatSync(pathname, { bigint: true });
|
|
2748
|
+
if (
|
|
2749
|
+
!afterCount.isDirectory() ||
|
|
2750
|
+
afterCount.isSymbolicLink() ||
|
|
2751
|
+
!managedGcSameDirectoryEvidence(before, managedGcDirectoryEvidence(afterCount))
|
|
2752
|
+
)
|
|
2753
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
2754
|
+
const collected = scan(true);
|
|
2755
|
+
managedGcReserveInventoryWork(operation);
|
|
2756
|
+
const afterCollect = fs.lstatSync(pathname, { bigint: true });
|
|
2757
|
+
if (
|
|
2758
|
+
collected.count !== counted.count ||
|
|
2759
|
+
collected.physicalEntryCount !== counted.physicalEntryCount ||
|
|
2760
|
+
!afterCollect.isDirectory() ||
|
|
2761
|
+
afterCollect.isSymbolicLink() ||
|
|
2762
|
+
!managedGcSameDirectoryEvidence(before, managedGcDirectoryEvidence(afterCollect))
|
|
2763
|
+
)
|
|
2764
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
2765
|
+
managedGcReserveInventoryWork(operation, collected.entries.length);
|
|
2766
|
+
collected.entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
2767
|
+
return {
|
|
2768
|
+
evidence: before,
|
|
2769
|
+
entryCount: counted.count,
|
|
2770
|
+
physicalEntryCount: counted.physicalEntryCount,
|
|
2771
|
+
entries: collected.entries,
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
function managedGcReceiptDirectoryInventory(
|
|
2776
|
+
scope: ManagedScope,
|
|
2777
|
+
store: ManagedSessionDescendantStore,
|
|
2778
|
+
operation: ManagedGcInventoryOperation,
|
|
2779
|
+
): ManagedGcDirectoryInventory {
|
|
2780
|
+
const relativePath = MANAGED_GC_RETIREMENT_RECEIPTS;
|
|
2781
|
+
managedGcReserveInventoryWork(
|
|
2782
|
+
operation,
|
|
2783
|
+
managedGcComponentInputAllowance(scope.directoryPath) + relativePath.length,
|
|
2784
|
+
);
|
|
2785
|
+
const pathname = path.join(scope.directoryPath, relativePath);
|
|
2786
|
+
const before = store.captureDirectoryIdentity(relativePath);
|
|
2787
|
+
const inventory = managedGcStreamDirectoryInventory(
|
|
2788
|
+
pathname,
|
|
2789
|
+
entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
|
|
2790
|
+
entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
|
|
2791
|
+
operation,
|
|
2792
|
+
);
|
|
2793
|
+
managedGcReserveInventoryWork(operation);
|
|
2794
|
+
const after = store.captureDirectoryIdentity(relativePath);
|
|
2795
|
+
if (
|
|
2796
|
+
before.dev !== after.dev ||
|
|
2797
|
+
before.ino !== after.ino ||
|
|
2798
|
+
before.dev !== inventory.evidence.dev ||
|
|
2799
|
+
before.ino !== inventory.evidence.ino
|
|
2800
|
+
)
|
|
2801
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
2802
|
+
managedGcReserveInventoryWork(operation);
|
|
2803
|
+
store.assertBound();
|
|
2804
|
+
return inventory;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
function managedGcReceiptCapacity(): never {
|
|
2808
|
+
throw new Error("managed_gc_receipt_capacity_exceeded");
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function managedGcJsonStringByteLength(value: string, add: (amount: number) => void): void {
|
|
2812
|
+
add(2);
|
|
2813
|
+
for (let index = 0; index < value.length; index++) {
|
|
2814
|
+
const unit = value.charCodeAt(index);
|
|
2815
|
+
if (unit === 0x22 || unit === 0x5c) add(2);
|
|
2816
|
+
else if (unit < 0x20)
|
|
2817
|
+
add(unit === 0x08 || unit === 0x09 || unit === 0x0a || unit === 0x0c || unit === 0x0d ? 2 : 6);
|
|
2818
|
+
else if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
2819
|
+
const low = value.charCodeAt(index + 1);
|
|
2820
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
2821
|
+
add(4);
|
|
2822
|
+
index++;
|
|
2823
|
+
} else add(6);
|
|
2824
|
+
} else if (unit >= 0xdc00 && unit <= 0xdfff) add(6);
|
|
2825
|
+
else if (unit <= 0x7f) add(1);
|
|
2826
|
+
else if (unit <= 0x7ff) add(2);
|
|
2827
|
+
else add(3);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
/** Measure the canonical JSON encoding before JSON.stringify or Buffer allocation. */
|
|
2832
|
+
function managedGcJsonByteLength(value: unknown): number {
|
|
2833
|
+
let bytes = 0;
|
|
2834
|
+
const active = new Set<object>();
|
|
2835
|
+
const add = (amount: number): void => {
|
|
2836
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || amount > MANAGED_GC_RECEIPT_MAX_BYTES - bytes)
|
|
2837
|
+
managedGcReceiptCapacity();
|
|
2838
|
+
bytes += amount;
|
|
2839
|
+
};
|
|
2840
|
+
const visit = (item: unknown, depth: number, inArray = false): void => {
|
|
2841
|
+
if (depth > 32) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
2842
|
+
if (item === null) {
|
|
2843
|
+
add(4);
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
if (typeof item === "string") {
|
|
2847
|
+
managedGcJsonStringByteLength(item, add);
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
if (typeof item === "bigint") {
|
|
2851
|
+
managedGcJsonStringByteLength(item.toString(), add);
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
if (typeof item === "boolean") {
|
|
2855
|
+
add(item ? 4 : 5);
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
if (typeof item === "number") {
|
|
2859
|
+
add(Number.isFinite(item) ? (Object.is(item, -0) ? 1 : String(item).length) : 4);
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
if (typeof item === "undefined" || typeof item === "function" || typeof item === "symbol") {
|
|
2863
|
+
if (inArray) add(4);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
if (typeof item !== "object") managedGcReceiptCapacity();
|
|
2867
|
+
if (active.has(item)) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
2868
|
+
active.add(item);
|
|
2869
|
+
const array = Array.isArray(item);
|
|
2870
|
+
if (array) {
|
|
2871
|
+
const values = item as unknown[];
|
|
2872
|
+
if (values.length > MANAGED_GC_RECEIPT_MAX_BYTES) managedGcReceiptCapacity();
|
|
2873
|
+
add(2);
|
|
2874
|
+
let emitted = false;
|
|
2875
|
+
for (let index = 0; index < values.length; index++) {
|
|
2876
|
+
if (emitted) add(1);
|
|
2877
|
+
emitted = true;
|
|
2878
|
+
visit(values[index], depth + 1, true);
|
|
2879
|
+
}
|
|
2880
|
+
} else {
|
|
2881
|
+
add(2);
|
|
2882
|
+
let emitted = false;
|
|
2883
|
+
for (const key in item) {
|
|
2884
|
+
if (!Object.hasOwn(item, key)) continue;
|
|
2885
|
+
const child = (item as Record<string, unknown>)[key];
|
|
2886
|
+
if (child === undefined || typeof child === "function" || typeof child === "symbol") continue;
|
|
2887
|
+
if (emitted) add(1);
|
|
2888
|
+
emitted = true;
|
|
2889
|
+
managedGcJsonStringByteLength(key, add);
|
|
2890
|
+
add(1);
|
|
2891
|
+
visit(child, depth + 1);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
active.delete(item);
|
|
2895
|
+
};
|
|
2896
|
+
visit(value, 0);
|
|
2897
|
+
add(1);
|
|
2898
|
+
return bytes;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
function validateManagedGcTranscriptPath(scope: ManagedScope, transcriptPath: string): string {
|
|
2902
|
+
if (
|
|
2903
|
+
typeof transcriptPath !== "string" ||
|
|
2904
|
+
!path.isAbsolute(transcriptPath) ||
|
|
2905
|
+
path.resolve(transcriptPath) !== transcriptPath ||
|
|
2906
|
+
path.dirname(transcriptPath) !== scope.directoryPath ||
|
|
2907
|
+
path.extname(transcriptPath) !== ".jsonl" ||
|
|
2908
|
+
!pathIsWithin(scope.sessionsRoot, transcriptPath)
|
|
2909
|
+
)
|
|
2910
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
2911
|
+
return transcriptPath;
|
|
2912
|
+
}
|
|
2913
|
+
function managedGcTargetFromSnapshot(
|
|
2914
|
+
scope: ManagedScope,
|
|
2915
|
+
transcriptPath: string,
|
|
2916
|
+
snapshot: SessionStorageSnapshot,
|
|
2917
|
+
): ManagedGcSessionRetirementTarget {
|
|
2918
|
+
if (
|
|
2919
|
+
!snapshot.stat.isFile ||
|
|
2920
|
+
snapshot.stat.nlink !== 1n ||
|
|
2921
|
+
snapshot.stat.size !== snapshot.bytes.byteLength ||
|
|
2922
|
+
!Number.isSafeInteger(snapshot.stat.size) ||
|
|
2923
|
+
snapshot.stat.size < 0
|
|
2924
|
+
)
|
|
2925
|
+
throw new Error("task_artifact_owner_continuation_identity_invalid");
|
|
2926
|
+
const header = parseFirstJsonlLine(snapshot.bytes);
|
|
2927
|
+
if (
|
|
2928
|
+
header?.type !== "session" ||
|
|
2929
|
+
typeof header.id !== "string" ||
|
|
2930
|
+
header.id.length === 0 ||
|
|
2931
|
+
typeof header.cwd !== "string" ||
|
|
2932
|
+
header.cwd.length === 0
|
|
2933
|
+
)
|
|
2934
|
+
throw new Error("task_artifact_owner_transcript_header_invalid");
|
|
2935
|
+
const cwdIdentity = identityFor(header.cwd);
|
|
2936
|
+
if (!cwdIdentity.ok || cwdIdentity.platform !== scope.platform || cwdIdentity.canonicalPath !== scope.canonicalCwd)
|
|
2937
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
2938
|
+
const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, header.id);
|
|
2939
|
+
if (!locator) throw new Error("task_artifact_owner_locator_missing");
|
|
2940
|
+
return Object.freeze({
|
|
2941
|
+
transcriptPath,
|
|
2942
|
+
sessionId: header.id,
|
|
2943
|
+
cwd: header.cwd,
|
|
2944
|
+
transcriptIdentity: {
|
|
2945
|
+
dev: snapshot.stat.dev,
|
|
2946
|
+
ino: snapshot.stat.ino,
|
|
2947
|
+
nlink: snapshot.stat.nlink,
|
|
2948
|
+
size: snapshot.stat.size,
|
|
2949
|
+
mtimeNs: snapshot.stat.mtimeNs,
|
|
2950
|
+
sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
2951
|
+
},
|
|
2952
|
+
taskArtifactOwnerLocator: locator,
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
/** Bind the first transcript header through FileSessionStorage's descriptor-bound, no-follow snapshot. */
|
|
2956
|
+
export function bindManagedGcSessionRetirementTarget(
|
|
2957
|
+
scope: ManagedScope,
|
|
2958
|
+
transcriptPathValue: string,
|
|
2959
|
+
): ManagedGcSessionRetirementTarget {
|
|
2960
|
+
const trusted = managedGcTrustedScope(scope);
|
|
2961
|
+
const transcriptPath = validateManagedGcTranscriptPath(scope, transcriptPathValue);
|
|
2962
|
+
const store = managedGcScopeReader(scope, trusted);
|
|
2963
|
+
try {
|
|
2964
|
+
store.verifyRootSecurity();
|
|
2965
|
+
store.assertBound();
|
|
2966
|
+
const snapshot = new FileSessionStorage().readSnapshotSync(transcriptPath);
|
|
2967
|
+
store.assertBound();
|
|
2968
|
+
return managedGcTargetFromSnapshot(scope, transcriptPath, snapshot);
|
|
2969
|
+
} finally {
|
|
2970
|
+
store.close();
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
function sameManagedGcTarget(left: ManagedGcSessionRetirementTarget, right: ManagedGcSessionRetirementTarget): boolean {
|
|
2974
|
+
return (
|
|
2975
|
+
left.transcriptPath === right.transcriptPath &&
|
|
2976
|
+
left.sessionId === right.sessionId &&
|
|
2977
|
+
left.cwd === right.cwd &&
|
|
2978
|
+
sameIdentity(left.transcriptIdentity, right.transcriptIdentity) &&
|
|
2979
|
+
util.isDeepStrictEqual(left.taskArtifactOwnerLocator, right.taskArtifactOwnerLocator)
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
function assertManagedGcTranscriptUnchangedOrAbsent(
|
|
2983
|
+
scope: ManagedScope,
|
|
2984
|
+
target: ManagedGcSessionRetirementTarget,
|
|
2985
|
+
allowAbsent: boolean,
|
|
2986
|
+
): void {
|
|
2987
|
+
try {
|
|
2988
|
+
const current = bindManagedGcSessionRetirementTarget(scope, target.transcriptPath);
|
|
2989
|
+
if (!sameManagedGcTarget(current, target))
|
|
2990
|
+
throw new Error("task_artifact_owner_continuation_transcript_replaced");
|
|
2991
|
+
} catch (error) {
|
|
2992
|
+
if (allowAbsent && hasFsCode(error, "ENOENT")) return;
|
|
2993
|
+
throw error;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
function managedGcReceiptRecord(
|
|
2997
|
+
scope: ManagedScope,
|
|
2998
|
+
receipt: ManagedGcSessionRetirementReceipt,
|
|
2999
|
+
attempt?: number,
|
|
3000
|
+
): Record<string, unknown> {
|
|
3001
|
+
const state = receipt.state;
|
|
3002
|
+
const common = {
|
|
3003
|
+
schemaVersion: 1,
|
|
3004
|
+
state,
|
|
3005
|
+
scope: scopeDigest(scope.platform, scope.canonicalCwd),
|
|
3006
|
+
transcriptPath: receipt.transcriptPath,
|
|
3007
|
+
sessionId: receipt.sessionId,
|
|
3008
|
+
cwd: receipt.cwd,
|
|
3009
|
+
transcriptIdentity: managedGcRetirementIdentityRecord(receipt.transcriptIdentity),
|
|
3010
|
+
taskArtifactOwnerDeletionEvidence: receipt.taskArtifactOwnerDeletionEvidence,
|
|
3011
|
+
};
|
|
3012
|
+
const hasOutcomeFields =
|
|
3013
|
+
receipt.ownerRetirementAttempt !== undefined ||
|
|
3014
|
+
receipt.taskArtifactOwnerRetirementOutcome !== undefined ||
|
|
3015
|
+
receipt.taskArtifactOwnerRetirementContinuation !== undefined ||
|
|
3016
|
+
receipt.taskArtifactOwnerPayloadRetired !== undefined ||
|
|
3017
|
+
receipt.taskArtifactOwnerNamespaceRetained !== undefined ||
|
|
3018
|
+
receipt.taskArtifactOwnerRetired !== undefined;
|
|
3019
|
+
if (state === "prepared" || state === "artifacts_removed") {
|
|
3020
|
+
if (
|
|
3021
|
+
hasOutcomeFields ||
|
|
3022
|
+
(state === "prepared" && receipt.artifactsRemoved !== undefined) ||
|
|
3023
|
+
(state === "artifacts_removed" && receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true)
|
|
3024
|
+
)
|
|
3025
|
+
throw new Error("task_artifact_owner_continuation_state_mismatch");
|
|
3026
|
+
return state === "prepared" ? common : { ...common, artifactsRemoved: true };
|
|
3027
|
+
}
|
|
3028
|
+
if (state === "owner_pending") {
|
|
3029
|
+
const outcome = receipt.taskArtifactOwnerRetirementOutcome;
|
|
3030
|
+
if (
|
|
3031
|
+
!outcome ||
|
|
3032
|
+
outcome.kind === "completed" ||
|
|
3033
|
+
(receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true) ||
|
|
3034
|
+
receipt.taskArtifactOwnerRetired !== undefined ||
|
|
3035
|
+
(attempt !== undefined &&
|
|
3036
|
+
receipt.ownerRetirementAttempt !== undefined &&
|
|
3037
|
+
receipt.ownerRetirementAttempt !== attempt) ||
|
|
3038
|
+
(receipt.taskArtifactOwnerRetirementContinuation !== undefined &&
|
|
3039
|
+
!util.isDeepStrictEqual(receipt.taskArtifactOwnerRetirementContinuation, outcome.continuation)) ||
|
|
3040
|
+
(receipt.taskArtifactOwnerPayloadRetired !== undefined &&
|
|
3041
|
+
receipt.taskArtifactOwnerPayloadRetired !== (outcome.kind === "payload_retired")) ||
|
|
3042
|
+
(receipt.taskArtifactOwnerNamespaceRetained !== undefined &&
|
|
3043
|
+
receipt.taskArtifactOwnerNamespaceRetained !== (outcome.kind === "payload_retired"))
|
|
3044
|
+
)
|
|
3045
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
3046
|
+
if (!Number.isSafeInteger(attempt) || attempt === undefined || attempt < 1)
|
|
3047
|
+
throw new Error("task_artifact_owner_continuation_attempt_invalid");
|
|
3048
|
+
return {
|
|
3049
|
+
...common,
|
|
3050
|
+
ownerRetirementAttempt: attempt,
|
|
3051
|
+
artifactsRemoved: true,
|
|
3052
|
+
taskArtifactOwnerRetirementOutcome: outcome,
|
|
3053
|
+
taskArtifactOwnerRetirementContinuation: outcome.continuation,
|
|
3054
|
+
...(outcome.kind === "payload_retired"
|
|
3055
|
+
? { taskArtifactOwnerPayloadRetired: true, taskArtifactOwnerNamespaceRetained: true }
|
|
3056
|
+
: {}),
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
if (
|
|
3060
|
+
(receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true) ||
|
|
3061
|
+
hasOutcomeFieldsExceptOutcome(receipt) ||
|
|
3062
|
+
(receipt.taskArtifactOwnerRetired !== undefined && receipt.taskArtifactOwnerRetired !== true) ||
|
|
3063
|
+
receipt.taskArtifactOwnerRetirementOutcome?.kind !== "completed"
|
|
3064
|
+
)
|
|
3065
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
3066
|
+
return {
|
|
3067
|
+
...common,
|
|
3068
|
+
artifactsRemoved: true,
|
|
3069
|
+
taskArtifactOwnerRetired: true,
|
|
3070
|
+
taskArtifactOwnerRetirementOutcome: receipt.taskArtifactOwnerRetirementOutcome,
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
function hasOutcomeFieldsExceptOutcome(receipt: ManagedGcSessionRetirementReceipt): boolean {
|
|
3075
|
+
return (
|
|
3076
|
+
receipt.ownerRetirementAttempt !== undefined ||
|
|
3077
|
+
receipt.taskArtifactOwnerRetirementContinuation !== undefined ||
|
|
3078
|
+
receipt.taskArtifactOwnerPayloadRetired !== undefined ||
|
|
3079
|
+
receipt.taskArtifactOwnerNamespaceRetained !== undefined
|
|
3080
|
+
);
|
|
3081
|
+
}
|
|
3082
|
+
function readManagedGcReceiptSnapshot(
|
|
3083
|
+
store: ManagedSessionDescendantStore,
|
|
3084
|
+
relativePath: string,
|
|
3085
|
+
history?: MutableManagedGcReceiptHistory,
|
|
3086
|
+
): ManagedFileSnapshot | null {
|
|
3087
|
+
try {
|
|
3088
|
+
return store.readExpectedBounded(relativePath, MANAGED_GC_RECEIPT_MAX_BYTES, (size, descriptor) => {
|
|
3089
|
+
if (descriptor.nlink !== 1n || descriptor.size !== size) throw new Error("source_changed");
|
|
3090
|
+
if (history) managedGcAdmitHistoryBytes(history, descriptor.size, relativePath);
|
|
3091
|
+
});
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
if (error instanceof Error && error.message === "artifact_capacity_exceeded") managedGcJournalCapacity();
|
|
3094
|
+
throw error;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
function managedGcAdmitHistoryBytes(history: MutableManagedGcReceiptHistory, size: number, relativePath: string): void {
|
|
3099
|
+
if (history.admittedFiles.has(relativePath)) return;
|
|
3100
|
+
if (history.admittedFiles.size >= MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
|
|
3101
|
+
if (size > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - history.encodedBytes) managedGcJournalCapacity();
|
|
3102
|
+
history.encodedBytes += size;
|
|
3103
|
+
history.admittedFiles.add(relativePath);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
function parseManagedGcReceiptFile(
|
|
3107
|
+
store: ManagedSessionDescendantStore,
|
|
3108
|
+
relativePath: string,
|
|
3109
|
+
scope: ManagedScope,
|
|
3110
|
+
ownerContext: TaskArtifactOwnerStorageContext,
|
|
3111
|
+
target: ManagedGcSessionRetirementTarget,
|
|
3112
|
+
history?: MutableManagedGcReceiptHistory,
|
|
3113
|
+
): ManagedGcSessionRetirementReceipt | undefined {
|
|
3114
|
+
const snapshot = readManagedGcReceiptSnapshot(store, relativePath, history);
|
|
3115
|
+
if (!snapshot) return undefined;
|
|
3116
|
+
let value: unknown;
|
|
3117
|
+
try {
|
|
3118
|
+
value = JSON.parse(snapshot.bytes.toString("utf8"));
|
|
3119
|
+
} catch {
|
|
3120
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3121
|
+
}
|
|
3122
|
+
return parseManagedGcRetirementReceipt(value, scopeDigest(scope.platform, scope.canonicalCwd), ownerContext, target);
|
|
3123
|
+
}
|
|
3124
|
+
function targetFromPreparedRecord(value: unknown, transcriptPath: string): ManagedGcSessionRetirementTarget {
|
|
3125
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3126
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3127
|
+
const record = value as Record<string, unknown>;
|
|
3128
|
+
if (
|
|
3129
|
+
record.state !== "prepared" ||
|
|
3130
|
+
record.transcriptPath !== transcriptPath ||
|
|
3131
|
+
typeof record.sessionId !== "string" ||
|
|
3132
|
+
typeof record.cwd !== "string"
|
|
3133
|
+
)
|
|
3134
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3135
|
+
const evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
|
|
3136
|
+
return {
|
|
3137
|
+
transcriptPath,
|
|
3138
|
+
sessionId: record.sessionId,
|
|
3139
|
+
cwd: record.cwd,
|
|
3140
|
+
transcriptIdentity: managedGcRetirementIdentityValue(record.transcriptIdentity),
|
|
3141
|
+
taskArtifactOwnerLocator: evidence.locator,
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
function deepSame(left: unknown, right: unknown): boolean {
|
|
3145
|
+
return util.isDeepStrictEqual(left, right);
|
|
3146
|
+
}
|
|
3147
|
+
function readManagedGcPreparedAuthority(
|
|
3148
|
+
scope: ManagedScope,
|
|
3149
|
+
transcriptPath: string,
|
|
3150
|
+
ownerContext: TaskArtifactOwnerStorageContext,
|
|
3151
|
+
store: ManagedSessionDescendantStore,
|
|
3152
|
+
history?: MutableManagedGcReceiptHistory,
|
|
3153
|
+
bootstrap?: {
|
|
3154
|
+
readonly snapshot: ManagedFileSnapshot;
|
|
3155
|
+
readonly value: unknown;
|
|
3156
|
+
readonly history: MutableManagedGcReceiptHistory;
|
|
3157
|
+
},
|
|
3158
|
+
): ManagedGcSessionRetirementReceipt | undefined {
|
|
3159
|
+
const snapshot =
|
|
3160
|
+
bootstrap?.snapshot ??
|
|
3161
|
+
readManagedGcReceiptSnapshot(store, managedGcRetirementReceiptRelativePath(transcriptPath, "prepared"), history);
|
|
3162
|
+
if (!snapshot) return undefined;
|
|
3163
|
+
let value: unknown;
|
|
3164
|
+
if (bootstrap) {
|
|
3165
|
+
value = bootstrap.value;
|
|
3166
|
+
if (history && history !== bootstrap.history)
|
|
3167
|
+
managedGcAdmitHistoryBytes(
|
|
3168
|
+
history,
|
|
3169
|
+
snapshot.identity.size,
|
|
3170
|
+
managedGcRetirementReceiptRelativePath(transcriptPath, "prepared"),
|
|
3171
|
+
);
|
|
3172
|
+
} else {
|
|
3173
|
+
try {
|
|
3174
|
+
value = JSON.parse(snapshot.bytes.toString("utf8"));
|
|
3175
|
+
} catch {
|
|
3176
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
const target = targetFromPreparedRecord(value, transcriptPath);
|
|
3180
|
+
const prepared = parseManagedGcRetirementReceipt(
|
|
3181
|
+
value,
|
|
3182
|
+
scopeDigest(scope.platform, scope.canonicalCwd),
|
|
3183
|
+
ownerContext,
|
|
3184
|
+
target,
|
|
3185
|
+
);
|
|
3186
|
+
if (prepared.state !== "prepared") throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3187
|
+
return prepared;
|
|
3188
|
+
}
|
|
3189
|
+
function assertSameManagedGcImmutableAuthority(
|
|
3190
|
+
prepared: ManagedGcSessionRetirementReceipt,
|
|
3191
|
+
next: ManagedGcSessionRetirementReceipt,
|
|
3192
|
+
): void {
|
|
3193
|
+
if (
|
|
3194
|
+
!sameManagedGcTarget(prepared, next) ||
|
|
3195
|
+
!deepSame(prepared.taskArtifactOwnerDeletionEvidence, next.taskArtifactOwnerDeletionEvidence)
|
|
3196
|
+
)
|
|
3197
|
+
throw new Error("task_artifact_owner_continuation_evidence_mismatch");
|
|
3198
|
+
}
|
|
3199
|
+
function readManagedGcSessionRetirementReceiptUnlocked(
|
|
3200
|
+
scope: ManagedScope,
|
|
3201
|
+
transcriptPathValue: string,
|
|
3202
|
+
trusted: ManagedGcTrustedScope,
|
|
3203
|
+
store: ManagedSessionDescendantStore,
|
|
3204
|
+
operation: ManagedGcInventoryOperation,
|
|
3205
|
+
inventory?: ManagedGcDirectoryInventory,
|
|
3206
|
+
targetNames?: readonly string[],
|
|
3207
|
+
preparedBootstrap?: {
|
|
3208
|
+
readonly snapshot: ManagedFileSnapshot;
|
|
3209
|
+
readonly value: unknown;
|
|
3210
|
+
readonly history: MutableManagedGcReceiptHistory;
|
|
3211
|
+
},
|
|
3212
|
+
): {
|
|
3213
|
+
readonly receipt: ManagedGcSessionRetirementReceipt | undefined;
|
|
3214
|
+
readonly prepared: ManagedGcSessionRetirementReceipt | undefined;
|
|
3215
|
+
readonly history: ManagedGcReceiptHistory;
|
|
3216
|
+
} {
|
|
3217
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(transcriptPathValue));
|
|
3218
|
+
const transcriptPath = validateManagedGcTranscriptPath(scope, transcriptPathValue);
|
|
3219
|
+
managedGcReserveInventoryWork(operation);
|
|
3220
|
+
store.assertBound();
|
|
3221
|
+
const receiptInventory = inventory ?? managedGcReceiptDirectoryInventory(scope, store, operation);
|
|
3222
|
+
const names = targetNames ?? managedGcReceiptNames(scope, transcriptPath, store, operation, receiptInventory);
|
|
3223
|
+
if (names.length > MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
|
|
3224
|
+
const prefix = `${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-`;
|
|
3225
|
+
const pendingPrefix = `${prefix}owner_pending-`;
|
|
3226
|
+
const historyBytes = preparedBootstrap?.history ?? { encodedBytes: 0, admittedFiles: new Set<string>() };
|
|
3227
|
+
const history = (): ManagedGcReceiptHistory => ({
|
|
3228
|
+
entryCount: names.length,
|
|
3229
|
+
encodedBytes: historyBytes.encodedBytes,
|
|
3230
|
+
directoryEntryCount: receiptInventory.entryCount,
|
|
3231
|
+
directoryEvidence: receiptInventory.evidence,
|
|
3232
|
+
directoryInventory: receiptInventory,
|
|
3233
|
+
});
|
|
3234
|
+
const preparedName = `${prefix}prepared.json`;
|
|
3235
|
+
if (!names.includes(preparedName)) {
|
|
3236
|
+
if (names.length > 0) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3237
|
+
if (!inventory) {
|
|
3238
|
+
const after = managedGcReceiptDirectoryInventory(scope, store, operation);
|
|
3239
|
+
if (!managedGcSameReceiptDirectoryInventory(receiptInventory, after))
|
|
3240
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3241
|
+
}
|
|
3242
|
+
return { receipt: undefined, prepared: undefined, history: history() };
|
|
3243
|
+
}
|
|
3244
|
+
const prepared = readManagedGcPreparedAuthority(
|
|
3245
|
+
scope,
|
|
3246
|
+
transcriptPath,
|
|
3247
|
+
trusted.ownerContext,
|
|
3248
|
+
store,
|
|
3249
|
+
historyBytes,
|
|
3250
|
+
preparedBootstrap,
|
|
3251
|
+
);
|
|
3252
|
+
if (!prepared) {
|
|
3253
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3254
|
+
}
|
|
3255
|
+
const target = prepared;
|
|
3256
|
+
assertManagedGcTranscriptUnchangedOrAbsent(scope, target, true);
|
|
3257
|
+
managedGcReserveInventoryWork(operation, names.length);
|
|
3258
|
+
const nameSet = new Set(names);
|
|
3259
|
+
if (!nameSet.has(preparedName)) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3260
|
+
const readState = (state: ManagedGcSessionRetirementState, attempt?: number) =>
|
|
3261
|
+
parseManagedGcReceiptFile(
|
|
3262
|
+
store,
|
|
3263
|
+
managedGcRetirementReceiptRelativePath(transcriptPath, state, attempt),
|
|
3264
|
+
scope,
|
|
3265
|
+
trusted.ownerContext,
|
|
3266
|
+
target,
|
|
3267
|
+
historyBytes,
|
|
3268
|
+
);
|
|
3269
|
+
const artifactsName = path.posix.basename(
|
|
3270
|
+
managedGcRetirementReceiptRelativePath(transcriptPath, "artifacts_removed"),
|
|
3271
|
+
);
|
|
3272
|
+
const retiredName = path.posix.basename(managedGcRetirementReceiptRelativePath(transcriptPath, "owner_retired"));
|
|
3273
|
+
const artifactsRemoved = nameSet.has(artifactsName) ? readState("artifacts_removed") : undefined;
|
|
3274
|
+
const ownerRetired = nameSet.has(retiredName) ? readState("owner_retired") : undefined;
|
|
3275
|
+
if (
|
|
3276
|
+
(nameSet.has(artifactsName) && artifactsRemoved?.state !== "artifacts_removed") ||
|
|
3277
|
+
(nameSet.has(retiredName) && ownerRetired?.state !== "owner_retired")
|
|
3278
|
+
)
|
|
3279
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3280
|
+
managedGcReserveInventoryWork(operation, names.length);
|
|
3281
|
+
const attempts = names
|
|
3282
|
+
.filter(name => name.startsWith(pendingPrefix))
|
|
3283
|
+
.map(name => {
|
|
3284
|
+
const suffix = name.slice(pendingPrefix.length);
|
|
3285
|
+
if (!/^[0-9]{8,}\.json$/u.test(suffix)) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3286
|
+
const attempt = Number(suffix.slice(0, -5));
|
|
3287
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1 || `${String(attempt).padStart(8, "0")}.json` !== suffix)
|
|
3288
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3289
|
+
return attempt;
|
|
3290
|
+
})
|
|
3291
|
+
.sort((left, right) => left - right);
|
|
3292
|
+
let previousPending: ManagedGcSessionRetirementReceipt | undefined;
|
|
3293
|
+
let latestPending: ManagedGcSessionRetirementReceipt | undefined;
|
|
3294
|
+
for (let index = 0; index < attempts.length; index++) {
|
|
3295
|
+
const attempt = attempts[index]!;
|
|
3296
|
+
const pending = readState("owner_pending", attempt);
|
|
3297
|
+
if (
|
|
3298
|
+
attempt !== index + 1 ||
|
|
3299
|
+
!pending ||
|
|
3300
|
+
pending.state !== "owner_pending" ||
|
|
3301
|
+
pending.ownerRetirementAttempt !== attempt
|
|
3302
|
+
)
|
|
3303
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3304
|
+
assertSameManagedGcImmutableAuthority(prepared, pending);
|
|
3305
|
+
if (previousPending) {
|
|
3306
|
+
const previous = previousPending.taskArtifactOwnerRetirementOutcome;
|
|
3307
|
+
const next = pending.taskArtifactOwnerRetirementOutcome;
|
|
3308
|
+
if (
|
|
3309
|
+
!previous ||
|
|
3310
|
+
previous.kind === "completed" ||
|
|
3311
|
+
!next ||
|
|
3312
|
+
next.kind === "completed" ||
|
|
3313
|
+
!continuationExtendsPrevious(previous.continuation, next.continuation)
|
|
3314
|
+
)
|
|
3315
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3316
|
+
}
|
|
3317
|
+
previousPending = pending;
|
|
3318
|
+
latestPending = pending;
|
|
3319
|
+
}
|
|
3320
|
+
if ((attempts.length || ownerRetired) && !artifactsRemoved)
|
|
3321
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3322
|
+
if (artifactsRemoved) assertSameManagedGcImmutableAuthority(prepared, artifactsRemoved);
|
|
3323
|
+
if (ownerRetired) assertSameManagedGcImmutableAuthority(prepared, ownerRetired);
|
|
3324
|
+
if (ownerRetired) {
|
|
3325
|
+
const outcome = ownerRetired.taskArtifactOwnerRetirementOutcome;
|
|
3326
|
+
if (outcome?.kind !== "completed") throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
3327
|
+
verifyTaskArtifactOwnerPhysicalRetirement(
|
|
3328
|
+
trusted.ownerContext,
|
|
3329
|
+
ownerRetired.taskArtifactOwnerDeletionEvidence,
|
|
3330
|
+
outcome,
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
if (!inventory) {
|
|
3334
|
+
const after = managedGcReceiptDirectoryInventory(scope, store, operation);
|
|
3335
|
+
if (!managedGcSameReceiptDirectoryInventory(receiptInventory, after))
|
|
3336
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3337
|
+
}
|
|
3338
|
+
return { receipt: ownerRetired ?? latestPending ?? artifactsRemoved ?? prepared, prepared, history: history() };
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
function managedGcReceiptNames(
|
|
3342
|
+
scope: ManagedScope,
|
|
3343
|
+
transcriptPath: string,
|
|
3344
|
+
store: ManagedSessionDescendantStore,
|
|
3345
|
+
operation: ManagedGcInventoryOperation,
|
|
3346
|
+
inventory?: ManagedGcDirectoryInventory,
|
|
3347
|
+
): string[] {
|
|
3348
|
+
const prefix = `${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-`;
|
|
3349
|
+
const directoryInventory = inventory ?? managedGcReceiptDirectoryInventory(scope, store, operation);
|
|
3350
|
+
managedGcReserveInventoryWork(operation, directoryInventory.entries.length);
|
|
3351
|
+
const matching = directoryInventory.entries.filter(entry => entry.name.startsWith(prefix));
|
|
3352
|
+
if (matching.some(entry => !entry.isFile() || entry.isSymbolicLink()))
|
|
3353
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3354
|
+
managedGcReserveInventoryWork(operation, matching.length);
|
|
3355
|
+
const names = matching.map(entry => entry.name);
|
|
3356
|
+
const pendingPrefix = `${prefix}owner_pending-`;
|
|
3357
|
+
for (const name of names) {
|
|
3358
|
+
if (
|
|
3359
|
+
name !== `${prefix}prepared.json` &&
|
|
3360
|
+
name !== `${prefix}artifacts_removed.json` &&
|
|
3361
|
+
name !== `${prefix}owner_retired.json` &&
|
|
3362
|
+
!name.startsWith(pendingPrefix)
|
|
3363
|
+
)
|
|
3364
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3365
|
+
}
|
|
3366
|
+
return names;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
function managedGcSameReceiptDirectoryInventory(
|
|
3370
|
+
left: ManagedGcDirectoryInventory,
|
|
3371
|
+
right: ManagedGcDirectoryInventory,
|
|
3372
|
+
): boolean {
|
|
3373
|
+
return (
|
|
3374
|
+
left.entryCount === right.entryCount &&
|
|
3375
|
+
left.physicalEntryCount === right.physicalEntryCount &&
|
|
3376
|
+
managedGcSameDirectoryEvidence(left.evidence, right.evidence) &&
|
|
3377
|
+
left.entries.length === right.entries.length &&
|
|
3378
|
+
left.entries.every(
|
|
3379
|
+
(entry, index) =>
|
|
3380
|
+
entry.name === right.entries[index]?.name &&
|
|
3381
|
+
entry.isDirectory() === right.entries[index]?.isDirectory() &&
|
|
3382
|
+
entry.isFile() === right.entries[index]?.isFile() &&
|
|
3383
|
+
entry.isSymbolicLink() === right.entries[index]?.isSymbolicLink() &&
|
|
3384
|
+
entry.isBlockDevice() === right.entries[index]?.isBlockDevice() &&
|
|
3385
|
+
entry.isCharacterDevice() === right.entries[index]?.isCharacterDevice() &&
|
|
3386
|
+
entry.isFIFO() === right.entries[index]?.isFIFO() &&
|
|
3387
|
+
entry.isSocket() === right.entries[index]?.isSocket(),
|
|
3388
|
+
)
|
|
3389
|
+
);
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
function managedGcOptionalReceiptDirectoryInventory(
|
|
3393
|
+
scope: ManagedScope,
|
|
3394
|
+
store: ManagedSessionDescendantStore,
|
|
3395
|
+
operation: ManagedGcInventoryOperation,
|
|
3396
|
+
): ManagedGcDirectoryInventory | undefined {
|
|
3397
|
+
try {
|
|
3398
|
+
managedGcReserveInventoryWork(operation);
|
|
3399
|
+
store.captureDirectoryIdentity(MANAGED_GC_RETIREMENT_RECEIPTS);
|
|
3400
|
+
} catch (error) {
|
|
3401
|
+
if (hasFsCode(error, "ENOENT")) return undefined;
|
|
3402
|
+
throw error;
|
|
3403
|
+
}
|
|
3404
|
+
return managedGcReceiptDirectoryInventory(scope, store, operation);
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
function managedGcDiscoveryScopeFence(
|
|
3408
|
+
scope: ManagedScope,
|
|
3409
|
+
store: ManagedSessionDescendantStore,
|
|
3410
|
+
scopeIdentity: { readonly dev: string; readonly ino: string },
|
|
3411
|
+
binding: ManagedFileSnapshot,
|
|
3412
|
+
openReader: () => ManagedSessionDescendantStore,
|
|
3413
|
+
operation: ManagedGcInventoryOperation,
|
|
3414
|
+
): ManagedGcDiscoveryScopeFence {
|
|
3415
|
+
managedGcReserveInventoryWork(operation);
|
|
3416
|
+
const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
|
|
3417
|
+
managedGcReserveInventoryWork(operation);
|
|
3418
|
+
return {
|
|
3419
|
+
scope,
|
|
3420
|
+
scopeIdentity,
|
|
3421
|
+
binding,
|
|
3422
|
+
internalDirectoryIdentity,
|
|
3423
|
+
receiptInventory: managedGcOptionalReceiptDirectoryInventory(scope, store, operation),
|
|
3424
|
+
openReader,
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
function managedGcAssertDiscoveryScopeFence(
|
|
3429
|
+
fence: ManagedGcDiscoveryScopeFence,
|
|
3430
|
+
operation: ManagedGcInventoryOperation,
|
|
3431
|
+
): void {
|
|
3432
|
+
managedGcReserveInventoryWork(operation);
|
|
3433
|
+
const store = fence.openReader();
|
|
3434
|
+
let primaryError: unknown;
|
|
3435
|
+
let hasPrimaryError = false;
|
|
3436
|
+
try {
|
|
3437
|
+
managedGcReserveInventoryWork(operation);
|
|
3438
|
+
store.verifyRootSecurity();
|
|
3439
|
+
managedGcReserveInventoryWork(operation);
|
|
3440
|
+
const currentScope = fs.lstatSync(fence.scope.directoryPath, { bigint: true });
|
|
3441
|
+
if (
|
|
3442
|
+
!currentScope.isDirectory() ||
|
|
3443
|
+
currentScope.isSymbolicLink() ||
|
|
3444
|
+
currentScope.dev.toString() !== fence.scopeIdentity.dev ||
|
|
3445
|
+
currentScope.ino.toString() !== fence.scopeIdentity.ino
|
|
3446
|
+
)
|
|
3447
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3448
|
+
managedGcReserveInventoryWork(operation);
|
|
3449
|
+
const currentBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
3450
|
+
if (
|
|
3451
|
+
!currentBinding ||
|
|
3452
|
+
!util.isDeepStrictEqual(currentBinding.identity, fence.binding.identity) ||
|
|
3453
|
+
!currentBinding.bytes.equals(fence.binding.bytes)
|
|
3454
|
+
)
|
|
3455
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3456
|
+
managedGcReserveInventoryWork(operation);
|
|
3457
|
+
const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
|
|
3458
|
+
if (
|
|
3459
|
+
internalDirectoryIdentity.dev !== fence.internalDirectoryIdentity.dev ||
|
|
3460
|
+
internalDirectoryIdentity.ino !== fence.internalDirectoryIdentity.ino
|
|
3461
|
+
)
|
|
3462
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3463
|
+
const currentReceipts = managedGcOptionalReceiptDirectoryInventory(fence.scope, store, operation);
|
|
3464
|
+
if (
|
|
3465
|
+
(fence.receiptInventory === undefined) !== (currentReceipts === undefined) ||
|
|
3466
|
+
(fence.receiptInventory !== undefined &&
|
|
3467
|
+
currentReceipts !== undefined &&
|
|
3468
|
+
!managedGcSameReceiptDirectoryInventory(fence.receiptInventory, currentReceipts))
|
|
3469
|
+
)
|
|
3470
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3471
|
+
managedGcReserveInventoryWork(operation);
|
|
3472
|
+
store.assertBound();
|
|
3473
|
+
} catch (error) {
|
|
3474
|
+
primaryError = error;
|
|
3475
|
+
hasPrimaryError = true;
|
|
3476
|
+
}
|
|
3477
|
+
let closeError: unknown;
|
|
3478
|
+
try {
|
|
3479
|
+
store.close();
|
|
3480
|
+
} catch (error) {
|
|
3481
|
+
closeError = error;
|
|
3482
|
+
}
|
|
3483
|
+
if (hasPrimaryError) throw primaryError;
|
|
3484
|
+
if (closeError !== undefined) throw closeError;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
function assertManagedGcReceiptHistoryDirectoryUnchanged(
|
|
3488
|
+
scope: ManagedScope,
|
|
3489
|
+
store: ManagedSessionDescendantStore,
|
|
3490
|
+
history: ManagedGcReceiptHistory,
|
|
3491
|
+
operation: ManagedGcInventoryOperation,
|
|
3492
|
+
): void {
|
|
3493
|
+
const current = managedGcReceiptDirectoryInventory(scope, store, operation);
|
|
3494
|
+
if (
|
|
3495
|
+
!managedGcSameReceiptDirectoryInventory(history.directoryInventory, current) ||
|
|
3496
|
+
current.entryCount !== history.directoryEntryCount ||
|
|
3497
|
+
!managedGcSameDirectoryEvidence(current.evidence, history.directoryEvidence)
|
|
3498
|
+
)
|
|
3499
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
async function withManagedGcRetirementJournal<T>(
|
|
3503
|
+
scope: ManagedScope,
|
|
3504
|
+
transcriptPath: string,
|
|
3505
|
+
operation: (
|
|
3506
|
+
trusted: ManagedGcTrustedScope,
|
|
3507
|
+
store: ManagedSessionDescendantStore,
|
|
3508
|
+
lock: ManagedStorageLock,
|
|
3509
|
+
) => Promise<T> | T,
|
|
3510
|
+
): Promise<T> {
|
|
3511
|
+
const trusted = managedGcTrustedScope(scope);
|
|
3512
|
+
validateManagedGcTranscriptPath(scope, transcriptPath);
|
|
3513
|
+
const store = managedGcScopeStore(scope, trusted);
|
|
3514
|
+
let lock: ManagedStorageLock | undefined;
|
|
3515
|
+
let operationError: unknown;
|
|
3516
|
+
let result: T | undefined;
|
|
3517
|
+
let operationCompleted = false;
|
|
3518
|
+
let hasOperationError = false;
|
|
3519
|
+
try {
|
|
3520
|
+
store.verifyRootSecurity();
|
|
3521
|
+
store.ensureDirectory(MANAGED_GC_RETIREMENT_RECEIPTS);
|
|
3522
|
+
store.ensureDirectory(MANAGED_GC_RETIREMENT_LOCKS);
|
|
3523
|
+
lock = await acquireManagedLock(
|
|
3524
|
+
path.join(scope.directoryPath, MANAGED_GC_RETIREMENT_LOCKS),
|
|
3525
|
+
MANAGED_GC_RETIREMENT_SCOPE_LOCK_NAME,
|
|
3526
|
+
trusted.root,
|
|
3527
|
+
trusted.policy,
|
|
3528
|
+
);
|
|
3529
|
+
lock.assertOwned();
|
|
3530
|
+
store.assertBound();
|
|
3531
|
+
result = await operation(trusted, store, lock);
|
|
3532
|
+
operationCompleted = true;
|
|
3533
|
+
} catch (error) {
|
|
3534
|
+
operationError = error;
|
|
3535
|
+
hasOperationError = true;
|
|
3536
|
+
}
|
|
3537
|
+
let releaseError: unknown;
|
|
3538
|
+
let hasReleaseError = false;
|
|
3539
|
+
if (lock) {
|
|
3540
|
+
try {
|
|
3541
|
+
await lock.release();
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
releaseError = error;
|
|
3544
|
+
hasReleaseError = true;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
let closeError: unknown;
|
|
3548
|
+
let hasCloseError = false;
|
|
3549
|
+
try {
|
|
3550
|
+
store.close();
|
|
3551
|
+
} catch (error) {
|
|
3552
|
+
closeError = error;
|
|
3553
|
+
hasCloseError = true;
|
|
3554
|
+
}
|
|
3555
|
+
if (hasOperationError && (hasReleaseError || hasCloseError))
|
|
3556
|
+
throw new AggregateError(
|
|
3557
|
+
[operationError, ...(hasReleaseError ? [releaseError] : []), ...(hasCloseError ? [closeError] : [])],
|
|
3558
|
+
"managed_gc_retirement_operation_and_cleanup_failed",
|
|
3559
|
+
);
|
|
3560
|
+
if (hasOperationError) throw operationError;
|
|
3561
|
+
if (hasReleaseError) throw releaseError;
|
|
3562
|
+
if (hasCloseError) throw closeError;
|
|
3563
|
+
if (!operationCompleted) throw new Error("managed_gc_retirement_result_missing");
|
|
3564
|
+
return result as T;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
/** Read the prepared authority first, then strictly validate every later receipt against it. */
|
|
3568
|
+
export async function readManagedGcSessionRetirementReceipt(
|
|
3569
|
+
scope: ManagedScope,
|
|
3570
|
+
transcriptPath: string,
|
|
3571
|
+
): Promise<ManagedGcSessionRetirementReceipt | undefined> {
|
|
3572
|
+
const result = await withManagedGcRetirementJournal(scope, transcriptPath, (trusted, store, lock) =>
|
|
3573
|
+
readManagedGcSessionRetirementReceiptUnlocked(scope, transcriptPath, trusted, store, {
|
|
3574
|
+
kind: "leased-journal",
|
|
3575
|
+
lock,
|
|
3576
|
+
}),
|
|
3577
|
+
);
|
|
3578
|
+
return result.receipt;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
/** Read an authenticated journal without creating lock/receipt directories or repairing security state. */
|
|
3582
|
+
export async function readManagedGcSessionRetirementReceiptReadOnly(
|
|
3583
|
+
scope: ManagedScope,
|
|
3584
|
+
transcriptPath: string,
|
|
3585
|
+
): Promise<ManagedGcSessionRetirementReceipt | undefined> {
|
|
3586
|
+
const operation = newManagedGcInspectionOperation();
|
|
3587
|
+
managedGcReserveScopeComponentInputs(operation, scope);
|
|
3588
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(transcriptPath));
|
|
3589
|
+
const trusted = managedGcTrustedScope(scope);
|
|
3590
|
+
validateManagedGcTranscriptPath(scope, transcriptPath);
|
|
3591
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
3592
|
+
const store = managedGcScopeReader(scope, trusted);
|
|
3593
|
+
let receipt: ManagedGcSessionRetirementReceipt | undefined;
|
|
3594
|
+
let primaryError: unknown;
|
|
3595
|
+
let hasPrimaryError = false;
|
|
3596
|
+
try {
|
|
3597
|
+
managedGcReserveInventoryWork(operation);
|
|
3598
|
+
store.verifyRootSecurity();
|
|
3599
|
+
managedGcReserveInventoryWork(operation);
|
|
3600
|
+
store.assertBound();
|
|
3601
|
+
let receiptDirectoryPresent = true;
|
|
3602
|
+
try {
|
|
3603
|
+
managedGcReserveInventoryWork(operation);
|
|
3604
|
+
store.captureDirectoryIdentity(MANAGED_GC_RETIREMENT_RECEIPTS);
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
if (!hasFsCode(error, "ENOENT")) throw error;
|
|
3607
|
+
receiptDirectoryPresent = false;
|
|
3608
|
+
}
|
|
3609
|
+
if (receiptDirectoryPresent)
|
|
3610
|
+
receipt = readManagedGcSessionRetirementReceiptUnlocked(
|
|
3611
|
+
scope,
|
|
3612
|
+
transcriptPath,
|
|
3613
|
+
trusted,
|
|
3614
|
+
store,
|
|
3615
|
+
operation,
|
|
3616
|
+
).receipt;
|
|
3617
|
+
} catch (error) {
|
|
3618
|
+
primaryError = error;
|
|
3619
|
+
hasPrimaryError = true;
|
|
3620
|
+
}
|
|
3621
|
+
let closeError: unknown;
|
|
3622
|
+
try {
|
|
3623
|
+
store.close();
|
|
3624
|
+
} catch (error) {
|
|
3625
|
+
closeError = error;
|
|
3626
|
+
}
|
|
3627
|
+
if (hasPrimaryError) throw primaryError;
|
|
3628
|
+
if (closeError !== undefined) throw closeError;
|
|
3629
|
+
return receipt;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
function hasManagedGcRetirementJournalWithoutWorkspace(
|
|
3633
|
+
scope: ManagedScope,
|
|
3634
|
+
operation: ManagedGcInventoryOperation,
|
|
3635
|
+
onEmptyInventory?: (fence: ManagedGcDiscoveryScopeFence) => void,
|
|
3636
|
+
): boolean {
|
|
3637
|
+
managedGcReserveScopeComponentInputs(operation, scope);
|
|
3638
|
+
if (
|
|
3639
|
+
path.resolve(scope.canonicalCwd) !== scope.canonicalCwd ||
|
|
3640
|
+
scope.platform !== (process.platform === "win32" ? "win32" : "posix") ||
|
|
3641
|
+
scope.directoryName !== `v2-${scopeDigest(scope.platform, scope.canonicalCwd)}`
|
|
3642
|
+
)
|
|
3643
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3644
|
+
managedGcReserveInventoryWork(operation);
|
|
3645
|
+
const identity = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
3646
|
+
if (!identity.isDirectory() || identity.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
3647
|
+
const rootPath = configuredRootPath(scope);
|
|
3648
|
+
managedGcReserveInventoryWork(operation);
|
|
3649
|
+
const rootIdentity = fs.lstatSync(rootPath, { bigint: true });
|
|
3650
|
+
const assertRoot = (): void => {
|
|
3651
|
+
managedGcReserveInventoryWork(operation);
|
|
3652
|
+
const security = validateNativeSecurityResult(
|
|
3653
|
+
verifyExistingManagedScopeDirectory(rootPath),
|
|
3654
|
+
"verify",
|
|
3655
|
+
"directory",
|
|
3656
|
+
);
|
|
3657
|
+
managedGcReserveInventoryWork(operation);
|
|
3658
|
+
const current = fs.lstatSync(rootPath, { bigint: true });
|
|
3659
|
+
if (
|
|
3660
|
+
!security.ok ||
|
|
3661
|
+
!current.isDirectory() ||
|
|
3662
|
+
current.isSymbolicLink() ||
|
|
3663
|
+
current.dev !== rootIdentity.dev ||
|
|
3664
|
+
current.ino !== rootIdentity.ino
|
|
3665
|
+
)
|
|
3666
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3667
|
+
};
|
|
3668
|
+
assertRoot();
|
|
3669
|
+
managedGcReserveInventoryWork(operation);
|
|
3670
|
+
const rootAuthority = managedDirectoryRoot(rootPath);
|
|
3671
|
+
if (
|
|
3672
|
+
rootAuthority.dev !== BigInt.asUintN(64, rootIdentity.dev) ||
|
|
3673
|
+
rootAuthority.ino !== BigInt.asUintN(64, rootIdentity.ino)
|
|
3674
|
+
)
|
|
3675
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3676
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
3677
|
+
const store = new ManagedSessionDescendantStore(
|
|
3678
|
+
rootAuthority,
|
|
3679
|
+
scope.directoryPath,
|
|
3680
|
+
undefined,
|
|
3681
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
3682
|
+
scope.agentDir,
|
|
3683
|
+
{
|
|
3684
|
+
canonicalPath: scope.directoryPath,
|
|
3685
|
+
dev: BigInt.asUintN(64, identity.dev),
|
|
3686
|
+
ino: BigInt.asUintN(64, identity.ino),
|
|
3687
|
+
},
|
|
3688
|
+
"read-only",
|
|
3689
|
+
);
|
|
3690
|
+
let result: boolean | undefined;
|
|
3691
|
+
let primaryError: unknown;
|
|
3692
|
+
let hasPrimaryError = false;
|
|
3693
|
+
try {
|
|
3694
|
+
managedGcReserveInventoryWork(operation);
|
|
3695
|
+
store.verifyRootSecurity();
|
|
3696
|
+
managedGcReserveInventoryWork(operation);
|
|
3697
|
+
const binding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
3698
|
+
if (!binding || validateBindingRaw(scope, binding.bytes.toString("utf8")))
|
|
3699
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3700
|
+
const readNames = (): ManagedGcDirectoryInventory | undefined => {
|
|
3701
|
+
try {
|
|
3702
|
+
return managedGcReceiptDirectoryInventory(scope, store, operation);
|
|
3703
|
+
} catch (error) {
|
|
3704
|
+
if (hasFsCode(error, "ENOENT")) return undefined;
|
|
3705
|
+
throw error;
|
|
3706
|
+
}
|
|
3707
|
+
};
|
|
3708
|
+
const before = readNames();
|
|
3709
|
+
managedGcReserveInventoryWork(operation);
|
|
3710
|
+
const currentBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
3711
|
+
const after = readNames();
|
|
3712
|
+
if (
|
|
3713
|
+
!currentBinding ||
|
|
3714
|
+
!util.isDeepStrictEqual(binding.identity, currentBinding.identity) ||
|
|
3715
|
+
!binding.bytes.equals(currentBinding.bytes) ||
|
|
3716
|
+
(before === undefined) !== (after === undefined) ||
|
|
3717
|
+
(before !== undefined && after !== undefined && !managedGcSameReceiptDirectoryInventory(before, after))
|
|
3718
|
+
)
|
|
3719
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3720
|
+
managedGcReserveInventoryWork(operation);
|
|
3721
|
+
store.verifyRootSecurity();
|
|
3722
|
+
assertRoot();
|
|
3723
|
+
const hasJournal = before?.entries.some(entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX)) ?? false;
|
|
3724
|
+
if (!hasJournal && onEmptyInventory) {
|
|
3725
|
+
managedGcReserveInventoryWork(operation);
|
|
3726
|
+
const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
|
|
3727
|
+
const policy: ManagedSessionSecurityPolicy =
|
|
3728
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default";
|
|
3729
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
3730
|
+
onEmptyInventory({
|
|
3731
|
+
scope,
|
|
3732
|
+
scopeIdentity: { dev: identity.dev.toString(), ino: identity.ino.toString() },
|
|
3733
|
+
binding,
|
|
3734
|
+
internalDirectoryIdentity,
|
|
3735
|
+
receiptInventory: before,
|
|
3736
|
+
openReader: () => {
|
|
3737
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
3738
|
+
return new ManagedSessionDescendantStore(
|
|
3739
|
+
rootAuthority,
|
|
3740
|
+
scope.directoryPath,
|
|
3741
|
+
undefined,
|
|
3742
|
+
policy,
|
|
3743
|
+
scope.agentDir,
|
|
3744
|
+
{
|
|
3745
|
+
canonicalPath: scope.directoryPath,
|
|
3746
|
+
dev: BigInt.asUintN(64, identity.dev),
|
|
3747
|
+
ino: BigInt.asUintN(64, identity.ino),
|
|
3748
|
+
},
|
|
3749
|
+
"read-only",
|
|
3750
|
+
);
|
|
3751
|
+
},
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
result = hasJournal;
|
|
3755
|
+
} catch (error) {
|
|
3756
|
+
primaryError = error;
|
|
3757
|
+
hasPrimaryError = true;
|
|
3758
|
+
}
|
|
3759
|
+
let closeError: unknown;
|
|
3760
|
+
try {
|
|
3761
|
+
store.close();
|
|
3762
|
+
} catch (error) {
|
|
3763
|
+
closeError = error;
|
|
3764
|
+
}
|
|
3765
|
+
if (hasPrimaryError) throw primaryError;
|
|
3766
|
+
if (closeError !== undefined) throw closeError;
|
|
3767
|
+
return result as boolean;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
/** Discover complete owner journals under authenticated existing v2 scopes without initializing storage. */
|
|
3771
|
+
export async function discoverManagedGcSessionRetirementReceipts(input: {
|
|
3772
|
+
agentDir: string;
|
|
3773
|
+
sessionsRoot: string;
|
|
3774
|
+
}): Promise<readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[]> {
|
|
3775
|
+
const operation = newManagedGcInspectionOperation();
|
|
3776
|
+
return discoverManagedGcSessionRetirementReceiptsWithOperation(input, operation);
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
async function discoverManagedGcSessionRetirementReceiptsWithOperation(
|
|
3780
|
+
input: { agentDir: string; sessionsRoot: string },
|
|
3781
|
+
operation: ManagedGcInventoryOperation,
|
|
3782
|
+
): Promise<readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[]> {
|
|
3783
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(input.agentDir));
|
|
3784
|
+
const agentDir = canonicalizeTrustedPath(input.agentDir);
|
|
3785
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(input.sessionsRoot));
|
|
3786
|
+
const sessionsRoot = canonicalizeTrustedPath(input.sessionsRoot);
|
|
3787
|
+
managedGcReserveInventoryWork(operation);
|
|
3788
|
+
const profileStat = fs.lstatSync(agentDir, { bigint: true });
|
|
3789
|
+
managedGcReserveInventoryWork(operation);
|
|
3790
|
+
const rootStat = fs.lstatSync(sessionsRoot, { bigint: true });
|
|
3791
|
+
for (const [pathname, stat] of [
|
|
3792
|
+
[agentDir, profileStat],
|
|
3793
|
+
[sessionsRoot, rootStat],
|
|
3794
|
+
] as const) {
|
|
3795
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
3796
|
+
managedGcReserveInventoryWork(operation);
|
|
3797
|
+
const verified = validateNativeSecurityResult(
|
|
3798
|
+
verifyExistingManagedScopeDirectory(pathname),
|
|
3799
|
+
"verify",
|
|
3800
|
+
"directory",
|
|
3801
|
+
);
|
|
3802
|
+
if (!verified.ok) throw new Error("managed_gc_scope_authority_mismatch");
|
|
3803
|
+
}
|
|
3804
|
+
const inventoryBefore = {
|
|
3805
|
+
dev: rootStat.dev,
|
|
3806
|
+
ino: rootStat.ino,
|
|
3807
|
+
profileDev: profileStat.dev,
|
|
3808
|
+
profileIno: profileStat.ino,
|
|
3809
|
+
};
|
|
3810
|
+
managedGcReserveInventoryWork(operation);
|
|
3811
|
+
const rootInventory = managedGcStreamDirectoryInventory(
|
|
3812
|
+
sessionsRoot,
|
|
3813
|
+
entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
|
|
3814
|
+
entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
|
|
3815
|
+
operation,
|
|
3816
|
+
);
|
|
3817
|
+
if (rootInventory.evidence.dev !== rootStat.dev.toString() || rootInventory.evidence.ino !== rootStat.ino.toString())
|
|
3818
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3819
|
+
const entries = rootInventory.entries;
|
|
3820
|
+
managedGcReserveInventoryWork(operation, entries.length);
|
|
3821
|
+
const result: Array<{ readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }> = [];
|
|
3822
|
+
managedGcReserveInventoryWork(operation, entries.length);
|
|
3823
|
+
const scopeFences: ManagedGcDiscoveryScopeFence[] = [];
|
|
3824
|
+
// Fail closed for the whole root: a partial inventory cannot prove live sibling absence.
|
|
3825
|
+
for (const entry of entries) {
|
|
3826
|
+
if (!/^v2-[a-z2-7]{52}$/u.test(entry.name)) continue;
|
|
3827
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
|
|
3828
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
3829
|
+
const scopePath = path.join(sessionsRoot, entry.name);
|
|
3830
|
+
const bindingPath = path.join(scopePath, MANAGED_SESSION_BINDING_FILE);
|
|
3831
|
+
managedGcReserveInventoryWork(operation);
|
|
3832
|
+
const bindingBytes = captureManagedFileNoFollow(bindingPath).bytes;
|
|
3833
|
+
let bindingValue: unknown;
|
|
3834
|
+
try {
|
|
3835
|
+
bindingValue = JSON.parse(bindingBytes.toString("utf8"));
|
|
3836
|
+
} catch {
|
|
3837
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3838
|
+
}
|
|
3839
|
+
if (!isBinding(bindingValue) || bindingValue.identityDigest !== entry.name.slice(3))
|
|
3840
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3841
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(bindingValue.canonicalPath));
|
|
3842
|
+
const resolved = resolveManagedGcScopeForRead({ cwd: bindingValue.canonicalPath, agentDir, sessionsRoot });
|
|
3843
|
+
if (resolved.kind === "error" && resolved.code === "cwd_missing") {
|
|
3844
|
+
const storedScope: ManagedScope = {
|
|
3845
|
+
apiVersion: 1,
|
|
3846
|
+
layoutVersion: MANAGED_SESSION_LAYOUT_VERSION,
|
|
3847
|
+
identityVersion: MANAGED_SESSION_IDENTITY_VERSION,
|
|
3848
|
+
agentDir,
|
|
3849
|
+
sessionsRoot,
|
|
3850
|
+
canonicalCwd: bindingValue.canonicalPath,
|
|
3851
|
+
legacyLexicalCwd: bindingValue.canonicalPath,
|
|
3852
|
+
directoryName: entry.name,
|
|
3853
|
+
directoryPath: scopePath,
|
|
3854
|
+
platform: bindingValue.platform,
|
|
3855
|
+
};
|
|
3856
|
+
if (
|
|
3857
|
+
!hasManagedGcRetirementJournalWithoutWorkspace(storedScope, operation, fence => {
|
|
3858
|
+
managedGcReserveInventoryWork(operation);
|
|
3859
|
+
scopeFences.push(fence);
|
|
3860
|
+
})
|
|
3861
|
+
)
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
if (resolved.kind !== "resolved" || resolved.scope.directoryPath !== scopePath)
|
|
3865
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3866
|
+
const scope = resolved.scope;
|
|
3867
|
+
managedGcReserveScopeComponentInputs(operation, scope);
|
|
3868
|
+
const trusted = managedGcTrustedScope(scope);
|
|
3869
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
3870
|
+
const store = managedGcScopeReader(scope, trusted);
|
|
3871
|
+
const hasReceiptDirectory = managedGcWithClosedReader(store, () => {
|
|
3872
|
+
managedGcReserveInventoryWork(operation);
|
|
3873
|
+
const binding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
3874
|
+
if (
|
|
3875
|
+
!binding ||
|
|
3876
|
+
validateBindingRaw(scope, binding.bytes.toString("utf8")) ||
|
|
3877
|
+
!binding.bytes.equals(bindingBytes)
|
|
3878
|
+
)
|
|
3879
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3880
|
+
const scopeFence = managedGcDiscoveryScopeFence(
|
|
3881
|
+
scope,
|
|
3882
|
+
store,
|
|
3883
|
+
{ dev: trusted.identity.dev.toString(), ino: trusted.identity.ino.toString() },
|
|
3884
|
+
binding,
|
|
3885
|
+
() => {
|
|
3886
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
3887
|
+
return managedGcScopeReader(scope, trusted);
|
|
3888
|
+
},
|
|
3889
|
+
operation,
|
|
3890
|
+
);
|
|
3891
|
+
managedGcReserveInventoryWork(operation);
|
|
3892
|
+
scopeFences.push(scopeFence);
|
|
3893
|
+
const receiptInventory = scopeFence.receiptInventory;
|
|
3894
|
+
if (!receiptInventory) return false;
|
|
3895
|
+
managedGcReserveInventoryWork(operation, receiptInventory.physicalEntryCount);
|
|
3896
|
+
const groups = new Map<string, string[]>();
|
|
3897
|
+
for (const item of receiptInventory.entries) {
|
|
3898
|
+
if (!item.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX)) continue;
|
|
3899
|
+
if (
|
|
3900
|
+
!item.isFile() ||
|
|
3901
|
+
item.isSymbolicLink() ||
|
|
3902
|
+
!/^gc-retirement-[a-f0-9]{64}-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.test(
|
|
3903
|
+
item.name,
|
|
3904
|
+
)
|
|
3905
|
+
)
|
|
3906
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3907
|
+
managedGcReserveInventoryWork(operation);
|
|
3908
|
+
const key = item.name.slice(MANAGED_GC_RETIREMENT_PREFIX.length, MANAGED_GC_RETIREMENT_PREFIX.length + 64);
|
|
3909
|
+
let group = groups.get(key);
|
|
3910
|
+
if (!group) {
|
|
3911
|
+
managedGcReserveInventoryWork(operation);
|
|
3912
|
+
group = [];
|
|
3913
|
+
groups.set(key, group);
|
|
3914
|
+
}
|
|
3915
|
+
managedGcReserveInventoryWork(operation);
|
|
3916
|
+
group.push(item.name);
|
|
3917
|
+
}
|
|
3918
|
+
for (const [key, group] of groups) {
|
|
3919
|
+
const preparedName = `${MANAGED_GC_RETIREMENT_PREFIX}${key}-prepared.json`;
|
|
3920
|
+
if (!group.includes(preparedName)) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3921
|
+
managedGcReserveInventoryWork(operation);
|
|
3922
|
+
const bootstrapHistory: MutableManagedGcReceiptHistory = {
|
|
3923
|
+
encodedBytes: 0,
|
|
3924
|
+
admittedFiles: new Set<string>(),
|
|
3925
|
+
};
|
|
3926
|
+
const preparedFile = readManagedGcReceiptSnapshot(
|
|
3927
|
+
store,
|
|
3928
|
+
`${MANAGED_GC_RETIREMENT_RECEIPTS}/${preparedName}`,
|
|
3929
|
+
bootstrapHistory,
|
|
3930
|
+
);
|
|
3931
|
+
if (!preparedFile) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3932
|
+
let preparedValue: unknown;
|
|
3933
|
+
try {
|
|
3934
|
+
preparedValue = JSON.parse(preparedFile.bytes.toString("utf8"));
|
|
3935
|
+
} catch {
|
|
3936
|
+
throw new Error("task_artifact_owner_continuation_corrupt");
|
|
3937
|
+
}
|
|
3938
|
+
const transcriptPath =
|
|
3939
|
+
preparedValue && typeof preparedValue === "object"
|
|
3940
|
+
? (preparedValue as Record<string, unknown>).transcriptPath
|
|
3941
|
+
: undefined;
|
|
3942
|
+
if (typeof transcriptPath === "string") {
|
|
3943
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(transcriptPath));
|
|
3944
|
+
}
|
|
3945
|
+
if (typeof transcriptPath !== "string" || managedGcRetirementTranscriptKey(transcriptPath) !== key)
|
|
3946
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3947
|
+
const read = readManagedGcSessionRetirementReceiptUnlocked(
|
|
3948
|
+
scope,
|
|
3949
|
+
transcriptPath,
|
|
3950
|
+
trusted,
|
|
3951
|
+
store,
|
|
3952
|
+
operation,
|
|
3953
|
+
receiptInventory,
|
|
3954
|
+
group,
|
|
3955
|
+
{ snapshot: preparedFile, value: preparedValue, history: bootstrapHistory },
|
|
3956
|
+
);
|
|
3957
|
+
const receipt = read.receipt;
|
|
3958
|
+
if (!receipt) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
3959
|
+
managedGcReserveInventoryWork(operation);
|
|
3960
|
+
result.push({ scope, receipt });
|
|
3961
|
+
}
|
|
3962
|
+
const latestInventory = managedGcReceiptDirectoryInventory(scope, store, operation);
|
|
3963
|
+
if (!managedGcSameReceiptDirectoryInventory(receiptInventory, latestInventory))
|
|
3964
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
3965
|
+
return true;
|
|
3966
|
+
});
|
|
3967
|
+
if (!hasReceiptDirectory) continue;
|
|
3968
|
+
}
|
|
3969
|
+
for (const fence of scopeFences) managedGcAssertDiscoveryScopeFence(fence, operation);
|
|
3970
|
+
const rootInventoryAfter = managedGcStreamDirectoryInventory(
|
|
3971
|
+
sessionsRoot,
|
|
3972
|
+
entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
|
|
3973
|
+
entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
|
|
3974
|
+
operation,
|
|
3975
|
+
);
|
|
3976
|
+
if (!managedGcSameReceiptDirectoryInventory(rootInventory, rootInventoryAfter))
|
|
3977
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3978
|
+
managedGcReserveInventoryWork(operation);
|
|
3979
|
+
const rootAfter = fs.lstatSync(sessionsRoot, { bigint: true });
|
|
3980
|
+
managedGcReserveInventoryWork(operation);
|
|
3981
|
+
const profileAfter = fs.lstatSync(agentDir, { bigint: true });
|
|
3982
|
+
if (
|
|
3983
|
+
!rootAfter.isDirectory() ||
|
|
3984
|
+
rootAfter.isSymbolicLink() ||
|
|
3985
|
+
rootAfter.dev !== inventoryBefore.dev ||
|
|
3986
|
+
rootAfter.ino !== inventoryBefore.ino ||
|
|
3987
|
+
!profileAfter.isDirectory() ||
|
|
3988
|
+
profileAfter.isSymbolicLink() ||
|
|
3989
|
+
profileAfter.dev !== inventoryBefore.profileDev ||
|
|
3990
|
+
profileAfter.ino !== inventoryBefore.profileIno
|
|
3991
|
+
)
|
|
3992
|
+
throw new Error("managed_gc_scope_authority_mismatch");
|
|
3993
|
+
managedGcReserveInventoryWork(operation, result.length);
|
|
3994
|
+
return result.sort((left, right) => left.receipt.transcriptPath.localeCompare(right.receipt.transcriptPath));
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
const MANAGED_GC_PROTOCOL_ROLES: readonly ManagedGcProtocolRole[] = [
|
|
3998
|
+
MANAGED_LOCKS_DIRECTORY,
|
|
3999
|
+
MANAGED_RECEIPTS_DIRECTORY,
|
|
4000
|
+
MANAGED_TOMBSTONES_DIRECTORY,
|
|
4001
|
+
];
|
|
4002
|
+
const MANAGED_GC_PROTOCOL_MAX_ENTRIES = MANAGED_ARTIFACT_MAX_FILES;
|
|
4003
|
+
|
|
4004
|
+
function managedGcProtocolDirectoryStat(pathname: string): fs.BigIntStats {
|
|
4005
|
+
const stat = fs.lstatSync(pathname, { bigint: true });
|
|
4006
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_protocol_directory_invalid");
|
|
4007
|
+
const security = validateNativeSecurityResult(verifyExistingManagedScopeDirectory(pathname), "verify", "directory");
|
|
4008
|
+
if (!security.ok) throw new Error("managed_gc_protocol_directory_invalid");
|
|
4009
|
+
return stat;
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
function managedGcProtocolDirectorySnapshot(
|
|
4013
|
+
scope: ManagedScope,
|
|
4014
|
+
store: ManagedSessionDescendantStore,
|
|
4015
|
+
role: ManagedGcProtocolRole | typeof MANAGED_INTERNAL_DIRECTORY,
|
|
4016
|
+
operation: ManagedGcInventoryOperation,
|
|
4017
|
+
): { readonly path: string; readonly stat: fs.BigIntStats; readonly names: readonly string[] } {
|
|
4018
|
+
const relativePath = role === MANAGED_INTERNAL_DIRECTORY ? role : `${MANAGED_INTERNAL_DIRECTORY}/${role}`;
|
|
4019
|
+
managedGcReserveInventoryWork(
|
|
4020
|
+
operation,
|
|
4021
|
+
managedGcComponentInputAllowance(scope.directoryPath) + relativePath.length,
|
|
4022
|
+
);
|
|
4023
|
+
const pathname = path.join(scope.directoryPath, relativePath);
|
|
4024
|
+
managedGcReserveInventoryWork(operation);
|
|
4025
|
+
const before = managedGcProtocolDirectoryStat(pathname);
|
|
4026
|
+
managedGcReserveInventoryWork(operation);
|
|
4027
|
+
const retained = store.captureDirectoryIdentity(relativePath);
|
|
4028
|
+
if (retained.dev !== before.dev.toString() || retained.ino !== before.ino.toString())
|
|
4029
|
+
throw new Error("managed_gc_protocol_directory_identity_mismatch");
|
|
4030
|
+
const inventory = managedGcStreamDirectoryInventory(
|
|
4031
|
+
pathname,
|
|
4032
|
+
entry => role === MANAGED_RECEIPTS_DIRECTORY && entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
|
|
4033
|
+
() => true,
|
|
4034
|
+
operation,
|
|
4035
|
+
);
|
|
4036
|
+
managedGcReserveInventoryWork(operation, inventory.entries.length);
|
|
4037
|
+
const names = inventory.entries.map(entry => entry.name);
|
|
4038
|
+
managedGcReserveInventoryWork(operation);
|
|
4039
|
+
const after = managedGcProtocolDirectoryStat(pathname);
|
|
4040
|
+
if (
|
|
4041
|
+
after.dev !== before.dev ||
|
|
4042
|
+
after.ino !== before.ino ||
|
|
4043
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
4044
|
+
after.ctimeNs !== before.ctimeNs ||
|
|
4045
|
+
after.mode !== before.mode ||
|
|
4046
|
+
!managedGcSameDirectoryEvidence(inventory.evidence, managedGcDirectoryEvidence(before))
|
|
4047
|
+
)
|
|
4048
|
+
throw new Error("managed_gc_protocol_directory_changed");
|
|
4049
|
+
managedGcReserveInventoryWork(operation);
|
|
4050
|
+
store.assertBound();
|
|
4051
|
+
return { path: pathname, stat: before, names };
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
function managedGcProtocolFileSnapshot(
|
|
4055
|
+
scope: ManagedScope,
|
|
4056
|
+
store: ManagedSessionDescendantStore,
|
|
4057
|
+
relativePath: string,
|
|
4058
|
+
receiptHistories?: Map<string, MutableManagedGcReceiptHistory>,
|
|
4059
|
+
): {
|
|
4060
|
+
readonly snapshot: ManagedFileSnapshot;
|
|
4061
|
+
readonly identity: ManagedGcProtocolFileSnapshot;
|
|
4062
|
+
} {
|
|
4063
|
+
store.assertBound();
|
|
4064
|
+
const pathname = path.join(scope.directoryPath, relativePath);
|
|
4065
|
+
const before = fs.lstatSync(pathname, { bigint: true });
|
|
4066
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n)
|
|
4067
|
+
throw new Error("managed_gc_protocol_file_invalid");
|
|
4068
|
+
const native = nativeScope();
|
|
4069
|
+
const security = validateNativeSecurityResult(
|
|
4070
|
+
process.platform === "win32"
|
|
4071
|
+
? native.verifyOwnerOnlyPathSecurityExpected(pathname, "file", before.dev, before.ino)
|
|
4072
|
+
: native.verifyOwnerOnlyPathSecurity(pathname, "file"),
|
|
4073
|
+
"verify",
|
|
4074
|
+
"file",
|
|
4075
|
+
);
|
|
4076
|
+
if (!security.ok) throw new Error("managed_gc_protocol_file_invalid");
|
|
4077
|
+
let snapshot: ManagedFileSnapshot | null;
|
|
4078
|
+
if (isManagedGcRetirementReceiptPath(relativePath)) {
|
|
4079
|
+
let history: MutableManagedGcReceiptHistory | undefined;
|
|
4080
|
+
if (receiptHistories) {
|
|
4081
|
+
const match =
|
|
4082
|
+
/^gc-retirement-([a-f0-9]{64})-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.exec(
|
|
4083
|
+
path.posix.basename(relativePath),
|
|
4084
|
+
);
|
|
4085
|
+
if (!match) throw new Error("task_artifact_owner_continuation_corrupt");
|
|
4086
|
+
history = receiptHistories.get(match[1]!);
|
|
4087
|
+
if (!history) {
|
|
4088
|
+
history = { encodedBytes: 0, admittedFiles: new Set<string>() };
|
|
4089
|
+
receiptHistories.set(match[1]!, history);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
snapshot = readManagedGcReceiptSnapshot(store, relativePath, history);
|
|
4093
|
+
} else if (isManagedGcCleanupReceiptPath(relativePath)) {
|
|
4094
|
+
try {
|
|
4095
|
+
snapshot = store.readExpectedBounded(relativePath, CLEANUP_RECEIPT_REPLAY_MAX_BYTES);
|
|
4096
|
+
} catch (error) {
|
|
4097
|
+
if (error instanceof Error && error.message === "artifact_capacity_exceeded") cleanupReceiptReplayCapacity();
|
|
4098
|
+
throw error;
|
|
4099
|
+
}
|
|
4100
|
+
} else {
|
|
4101
|
+
snapshot = store.readExpected(relativePath);
|
|
4102
|
+
}
|
|
4103
|
+
if (!snapshot) throw new Error("managed_gc_protocol_file_missing");
|
|
4104
|
+
const after = fs.lstatSync(pathname, { bigint: true });
|
|
4105
|
+
if (
|
|
4106
|
+
!after.isFile() ||
|
|
4107
|
+
after.isSymbolicLink() ||
|
|
4108
|
+
before.dev !== after.dev ||
|
|
4109
|
+
before.ino !== after.ino ||
|
|
4110
|
+
before.nlink !== after.nlink ||
|
|
4111
|
+
before.size !== after.size ||
|
|
4112
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
4113
|
+
before.ctimeNs !== after.ctimeNs ||
|
|
4114
|
+
before.mode !== after.mode ||
|
|
4115
|
+
snapshot.identity.dev !== before.dev ||
|
|
4116
|
+
snapshot.identity.ino !== before.ino ||
|
|
4117
|
+
snapshot.identity.nlink !== before.nlink ||
|
|
4118
|
+
snapshot.identity.size !== Number(before.size) ||
|
|
4119
|
+
snapshot.identity.mtimeNs !== before.mtimeNs ||
|
|
4120
|
+
snapshot.identity.ctimeNs !== before.ctimeNs
|
|
4121
|
+
)
|
|
4122
|
+
throw new Error("managed_gc_protocol_file_changed");
|
|
4123
|
+
store.assertBound();
|
|
4124
|
+
return {
|
|
4125
|
+
snapshot,
|
|
4126
|
+
identity: {
|
|
4127
|
+
name: path.basename(pathname),
|
|
4128
|
+
dev: snapshot.identity.dev.toString(),
|
|
4129
|
+
ino: snapshot.identity.ino.toString(),
|
|
4130
|
+
nlink: snapshot.identity.nlink.toString(),
|
|
4131
|
+
size: snapshot.identity.size,
|
|
4132
|
+
mtimeNs: snapshot.identity.mtimeNs.toString(),
|
|
4133
|
+
ctimeNs: snapshot.identity.ctimeNs.toString(),
|
|
4134
|
+
mode: Number(before.mode & 0o777n),
|
|
4135
|
+
sha256: snapshot.identity.sha256,
|
|
4136
|
+
},
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
function isManagedGcPendingCleanupReceiptPath(relativePath: string): boolean {
|
|
4141
|
+
const prefix = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_TOMBSTONES_DIRECTORY}/`;
|
|
4142
|
+
if (!relativePath.startsWith(prefix)) return false;
|
|
4143
|
+
return /\.cleanup-pending-[1-9][0-9]*\.json$/u.test(relativePath.slice(prefix.length));
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
function isManagedGcCleanupReceiptPath(relativePath: string): boolean {
|
|
4147
|
+
const prefix = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_TOMBSTONES_DIRECTORY}/`;
|
|
4148
|
+
if (!relativePath.startsWith(prefix)) return false;
|
|
4149
|
+
const match =
|
|
4150
|
+
/^[a-f0-9]{64}\.[a-f0-9]{64}\.cleanup-(pending|artifacts_removed|completed)-([1-9][0-9]*)\.json$/u.exec(
|
|
4151
|
+
relativePath.slice(prefix.length),
|
|
4152
|
+
);
|
|
4153
|
+
if (!match) return false;
|
|
4154
|
+
const attempt = Number(match[2]);
|
|
4155
|
+
return Number.isSafeInteger(attempt) && String(attempt) === match[2] && (match[1] !== "completed" || attempt === 1);
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
function isManagedGcRetirementReceiptPath(relativePath: string): boolean {
|
|
4159
|
+
return relativePath.startsWith(`${MANAGED_GC_RETIREMENT_RECEIPTS}/${MANAGED_GC_RETIREMENT_PREFIX}`);
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
function isManagedGcCleanupLikeName(name: string): boolean {
|
|
4163
|
+
return name.includes(".cleanup-") && name.endsWith(".json");
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
function managedGcProtocolRecord(
|
|
4167
|
+
bytes: Uint8Array,
|
|
4168
|
+
role: ManagedGcProtocolRole,
|
|
4169
|
+
name: string,
|
|
4170
|
+
): Record<string, unknown> {
|
|
4171
|
+
try {
|
|
4172
|
+
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
4173
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
4174
|
+
} catch {
|
|
4175
|
+
// A role-specific parser below supplies the bounded protocol location.
|
|
4176
|
+
}
|
|
4177
|
+
throw new Error(`managed_gc_protocol_record_invalid:${role}/${name}`);
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
function managedGcProtocolFileIdentityMatches(
|
|
4181
|
+
left: ManagedGcProtocolFileSnapshot,
|
|
4182
|
+
right: ManagedGcProtocolFileSnapshot,
|
|
4183
|
+
): boolean {
|
|
4184
|
+
return (
|
|
4185
|
+
left.name === right.name &&
|
|
4186
|
+
left.dev === right.dev &&
|
|
4187
|
+
left.ino === right.ino &&
|
|
4188
|
+
left.nlink === right.nlink &&
|
|
4189
|
+
left.size === right.size &&
|
|
4190
|
+
left.mtimeNs === right.mtimeNs &&
|
|
4191
|
+
left.ctimeNs === right.ctimeNs &&
|
|
4192
|
+
left.mode === right.mode &&
|
|
4193
|
+
left.sha256 === right.sha256
|
|
4194
|
+
);
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
function managedGcProtocolPathIdentity(pathname: string): ManagedGcProtocolPathIdentity {
|
|
4198
|
+
const stat = managedGcProtocolDirectoryStat(pathname);
|
|
4199
|
+
return { path: pathname, dev: stat.dev.toString(), ino: stat.ino.toString() };
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
function validateManagedGcProtocolMigrationReceipt(
|
|
4203
|
+
scope: ManagedScope,
|
|
4204
|
+
pathname: string,
|
|
4205
|
+
name: string,
|
|
4206
|
+
bytes: Uint8Array,
|
|
4207
|
+
): void {
|
|
4208
|
+
const match = /^([a-f0-9]{64})(?:\.(prepared|detached|published))?\.json$/u.exec(name);
|
|
4209
|
+
if (!match || match[2] !== undefined) throw new Error("managed_gc_protocol_migration_incomplete");
|
|
4210
|
+
const record = managedGcProtocolRecord(bytes, MANAGED_RECEIPTS_DIRECTORY, name);
|
|
4211
|
+
if (record.state !== "committed") throw new Error("managed_gc_protocol_migration_incomplete");
|
|
4212
|
+
const source = record.source as { path?: unknown } | undefined;
|
|
4213
|
+
const destination = record.destination as { path?: unknown } | undefined;
|
|
4214
|
+
if (typeof source?.path !== "string" || typeof destination?.path !== "string")
|
|
4215
|
+
throw new Error("managed_gc_protocol_migration_invalid");
|
|
4216
|
+
const legacy = inspectCandidate(source.path, "legacy");
|
|
4217
|
+
const managed = inspectCandidate(destination.path, "v2");
|
|
4218
|
+
if ("code" in legacy || "code" in managed || !receiptMatches(pathname, legacy, managed, scope))
|
|
4219
|
+
throw new Error("managed_gc_protocol_migration_authority_mismatch");
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
function validateManagedGcProtocolTombstoneFile(
|
|
4223
|
+
scope: ManagedScope,
|
|
4224
|
+
pathname: string,
|
|
4225
|
+
name: string,
|
|
4226
|
+
bytes: Uint8Array,
|
|
4227
|
+
journals: readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[],
|
|
4228
|
+
): void {
|
|
4229
|
+
if (/^[a-f0-9]{64}\.json$/u.test(name)) {
|
|
4230
|
+
const targets = retiredTargets(scope, pathname);
|
|
4231
|
+
if (!targets || targets.length === 0) throw new Error("managed_gc_protocol_tombstone_invalid");
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
4234
|
+
const match =
|
|
4235
|
+
/^([a-f0-9]{64})\.([a-f0-9]{64})\.cleanup-(pending|artifacts_removed|completed)-([1-9][0-9]*)\.json$/u.exec(name);
|
|
4236
|
+
if (!match || (match[3] === "completed" && match[4] !== "1"))
|
|
4237
|
+
throw new Error("managed_gc_protocol_tombstone_role_invalid");
|
|
4238
|
+
const tombstone = path.join(path.dirname(pathname), `${match[1]}.json`);
|
|
4239
|
+
const targets = retiredTargets(scope, tombstone);
|
|
4240
|
+
if (!targets) throw new Error("managed_gc_protocol_tombstone_authority_missing");
|
|
4241
|
+
const target = targets.find(candidate => stableOperationName(candidate) === match[2]);
|
|
4242
|
+
if (!target) throw new Error("managed_gc_protocol_tombstone_target_missing");
|
|
4243
|
+
const attempt = Number(match[4]);
|
|
4244
|
+
if (match[3] === "pending") {
|
|
4245
|
+
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
4246
|
+
if (!pending || pending.attempt < attempt) throw new Error("managed_gc_protocol_tombstone_history_invalid");
|
|
4247
|
+
return;
|
|
4248
|
+
}
|
|
4249
|
+
if (match[3] === "artifacts_removed") {
|
|
4250
|
+
if (!cleanupArtifactsRemovedEvidence(scope, tombstone, target, attempt))
|
|
4251
|
+
throw new Error("managed_gc_protocol_tombstone_artifacts_invalid");
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
|
|
4255
|
+
? journals.find(
|
|
4256
|
+
item => item.scope.directoryPath === scope.directoryPath && item.receipt.transcriptPath === target.path,
|
|
4257
|
+
)?.receipt
|
|
4258
|
+
: undefined;
|
|
4259
|
+
if (target.taskArtifactOwnerDeletionEvidence && !ownerReceipt)
|
|
4260
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
4261
|
+
const record = managedGcProtocolRecord(bytes, MANAGED_TOMBSTONES_DIRECTORY, name);
|
|
4262
|
+
if (!cleanupCompletedRecordMatches(scope, tombstone, target, ownerReceipt, record))
|
|
4263
|
+
throw new Error("managed_gc_protocol_tombstone_completion_invalid");
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4266
|
+
/** Inspect under a live operation lease; returned snapshots never authorize effects. */
|
|
4267
|
+
export function managedGcProtocolInspectorForLock(
|
|
4268
|
+
scope: ManagedScope,
|
|
4269
|
+
lock: ManagedStorageLock,
|
|
4270
|
+
): ManagedGcProtocolScopeInspector {
|
|
4271
|
+
const inspect = managedGcProtocolScopeInspectorWithOperation(scope);
|
|
4272
|
+
return async inputs => {
|
|
4273
|
+
const operation = newManagedGcInspectionOperation();
|
|
4274
|
+
managedGcReserveScopeComponentInputs(operation, scope);
|
|
4275
|
+
managedGcReserveInventoryWork(operation);
|
|
4276
|
+
lock.assertOwned();
|
|
4277
|
+
managedGcTrustedScope(scope);
|
|
4278
|
+
const snapshots = await inspect(inputs, operation);
|
|
4279
|
+
lock.assertOwned();
|
|
4280
|
+
managedGcReserveScopeComponentInputs(operation, scope);
|
|
4281
|
+
managedGcTrustedScope(scope);
|
|
4282
|
+
return snapshots;
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
/** Bind a read-only protocol inventory to an already prepared scope's root/profile/session identities. */
|
|
4287
|
+
export function managedGcProtocolScopeInspectorForScope(scope: ManagedScope): ManagedGcProtocolScopeInspector {
|
|
4288
|
+
const inspect = managedGcProtocolScopeInspectorWithOperation(scope);
|
|
4289
|
+
return async inputs => {
|
|
4290
|
+
const operation = newManagedGcInspectionOperation();
|
|
4291
|
+
return inspect(inputs, operation);
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
function managedGcProtocolScopeInspectorWithOperation(
|
|
4296
|
+
scope: ManagedScope,
|
|
4297
|
+
): (
|
|
4298
|
+
inputs: readonly ManagedGcProtocolScopeInput[],
|
|
4299
|
+
operation: ManagedGcInventoryOperation,
|
|
4300
|
+
) => Promise<readonly ManagedGcProtocolScopeSnapshot[]> {
|
|
4301
|
+
const trusted = managedGcTrustedScope(scope);
|
|
4302
|
+
const configuredRoot = managedGcProtocolPathIdentity(trusted.root.canonicalPath);
|
|
4303
|
+
const profile = managedGcProtocolPathIdentity(scope.agentDir);
|
|
4304
|
+
const sessionsRoot = managedGcProtocolPathIdentity(scope.sessionsRoot);
|
|
4305
|
+
const protocol = managedGcProtocolPathIdentity(path.join(scope.directoryPath, MANAGED_INTERNAL_DIRECTORY));
|
|
4306
|
+
const authorityIdentities = [configuredRoot, profile, sessionsRoot, protocol] as const;
|
|
4307
|
+
const assertAuthority = (operation: ManagedGcInventoryOperation): void => {
|
|
4308
|
+
managedGcReserveScopeComponentInputs(operation, scope);
|
|
4309
|
+
managedGcReserveInventoryWork(operation);
|
|
4310
|
+
managedGcTrustedScope(scope);
|
|
4311
|
+
for (const expected of authorityIdentities) {
|
|
4312
|
+
managedGcReserveInventoryWork(operation);
|
|
4313
|
+
const current = managedGcProtocolPathIdentity(expected.path);
|
|
4314
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino)
|
|
4315
|
+
throw new Error("managed_gc_protocol_root_identity_mismatch");
|
|
4316
|
+
}
|
|
4317
|
+
};
|
|
4318
|
+
return async (inputs: readonly ManagedGcProtocolScopeInput[], operation: ManagedGcInventoryOperation) => {
|
|
4319
|
+
if (inputs.length > MANAGED_GC_PROTOCOL_MAX_ENTRIES) throw new Error("managed_gc_protocol_scope_limit_exceeded");
|
|
4320
|
+
managedGcReserveInventoryWork(operation, inputs.length);
|
|
4321
|
+
assertAuthority(operation);
|
|
4322
|
+
const journals = await discoverManagedGcSessionRetirementReceiptsWithOperation(
|
|
4323
|
+
{ agentDir: scope.agentDir, sessionsRoot: scope.sessionsRoot },
|
|
4324
|
+
operation,
|
|
4325
|
+
);
|
|
4326
|
+
assertAuthority(operation);
|
|
4327
|
+
managedGcReserveInventoryWork(operation);
|
|
4328
|
+
managedGcReserveInventoryWork(operation);
|
|
4329
|
+
const snapshots: ManagedGcProtocolScopeSnapshot[] = [];
|
|
4330
|
+
let totalFiles = 0;
|
|
4331
|
+
for (const input of inputs) {
|
|
4332
|
+
if (typeof input.scopePath !== "string" || typeof input.protocolIdentity?.path !== "string")
|
|
4333
|
+
throw new Error("managed_gc_protocol_scope_path_mismatch");
|
|
4334
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(input.scopePath));
|
|
4335
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(input.protocolIdentity.path));
|
|
4336
|
+
if (
|
|
4337
|
+
path.resolve(input.scopePath) !== input.scopePath ||
|
|
4338
|
+
path.dirname(input.scopePath) !== scope.sessionsRoot ||
|
|
4339
|
+
!/^v2-[a-z2-7]{52}$/u.test(path.basename(input.scopePath)) ||
|
|
4340
|
+
input.scopeIdentity.path !== input.scopePath ||
|
|
4341
|
+
input.protocolIdentity.path !== path.join(input.scopePath, MANAGED_INTERNAL_DIRECTORY)
|
|
4342
|
+
)
|
|
4343
|
+
throw new Error("managed_gc_protocol_scope_path_mismatch");
|
|
4344
|
+
managedGcReserveInventoryWork(operation, 3);
|
|
4345
|
+
const bindingPath = path.join(input.scopePath, MANAGED_SESSION_BINDING_FILE);
|
|
4346
|
+
const binding = captureManagedFileNoFollow(bindingPath);
|
|
4347
|
+
managedGcReserveInventoryWork(operation);
|
|
4348
|
+
const bindingStat = fs.lstatSync(bindingPath, { bigint: true });
|
|
4349
|
+
const bindingIdentity: ManagedGcProtocolFileSnapshot = {
|
|
4350
|
+
name: MANAGED_SESSION_BINDING_FILE,
|
|
4351
|
+
dev: binding.identity.dev.toString(),
|
|
4352
|
+
ino: binding.identity.ino.toString(),
|
|
4353
|
+
nlink: binding.identity.nlink.toString(),
|
|
4354
|
+
size: binding.identity.size,
|
|
4355
|
+
mtimeNs: binding.identity.mtimeNs.toString(),
|
|
4356
|
+
ctimeNs: binding.identity.ctimeNs.toString(),
|
|
4357
|
+
mode: Number(bindingStat.mode & 0o777n),
|
|
4358
|
+
sha256: binding.identity.sha256,
|
|
4359
|
+
};
|
|
4360
|
+
if (!managedGcProtocolFileIdentityMatches(bindingIdentity, input.bindingIdentity))
|
|
4361
|
+
throw new Error("managed_gc_protocol_binding_identity_mismatch");
|
|
4362
|
+
const bindingValue: unknown = JSON.parse(binding.bytes.toString("utf8"));
|
|
4363
|
+
if (!isBinding(bindingValue)) throw new Error("managed_gc_scope_authority_mismatch");
|
|
4364
|
+
managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(bindingValue.canonicalPath));
|
|
4365
|
+
const resolved = resolveManagedGcScopeForRead({
|
|
4366
|
+
cwd: bindingValue.canonicalPath,
|
|
4367
|
+
agentDir: scope.agentDir,
|
|
4368
|
+
sessionsRoot: scope.sessionsRoot,
|
|
4369
|
+
});
|
|
4370
|
+
let candidateScope: ManagedScope;
|
|
4371
|
+
let candidateTrusted: ManagedGcTrustedScope | undefined;
|
|
4372
|
+
if (resolved.kind === "error" && resolved.code === "cwd_missing") {
|
|
4373
|
+
candidateScope = {
|
|
4374
|
+
apiVersion: 1,
|
|
4375
|
+
layoutVersion: MANAGED_SESSION_LAYOUT_VERSION,
|
|
4376
|
+
identityVersion: MANAGED_SESSION_IDENTITY_VERSION,
|
|
4377
|
+
agentDir: scope.agentDir,
|
|
4378
|
+
sessionsRoot: scope.sessionsRoot,
|
|
4379
|
+
canonicalCwd: bindingValue.canonicalPath,
|
|
4380
|
+
legacyLexicalCwd: bindingValue.canonicalPath,
|
|
4381
|
+
directoryName: path.basename(input.scopePath),
|
|
4382
|
+
directoryPath: input.scopePath,
|
|
4383
|
+
platform: bindingValue.platform,
|
|
4384
|
+
};
|
|
4385
|
+
if (hasManagedGcRetirementJournalWithoutWorkspace(candidateScope, operation))
|
|
4386
|
+
throw new Error("managed_gc_protocol_scope_unrecognized");
|
|
4387
|
+
} else if (resolved.kind === "resolved" && resolved.scope.directoryPath === input.scopePath) {
|
|
4388
|
+
candidateScope = resolved.scope;
|
|
4389
|
+
managedGcReserveScopeComponentInputs(operation, candidateScope);
|
|
4390
|
+
candidateTrusted = managedGcTrustedScope(candidateScope);
|
|
4391
|
+
} else {
|
|
4392
|
+
throw new Error("managed_gc_protocol_scope_unrecognized");
|
|
4393
|
+
}
|
|
4394
|
+
if (
|
|
4395
|
+
configuredRootPath(candidateScope) !== trusted.root.canonicalPath ||
|
|
4396
|
+
(candidateTrusted &&
|
|
4397
|
+
(candidateTrusted.root.canonicalPath !== trusted.root.canonicalPath ||
|
|
4398
|
+
candidateTrusted.root.dev !== trusted.root.dev ||
|
|
4399
|
+
candidateTrusted.root.ino !== trusted.root.ino)) ||
|
|
4400
|
+
candidateScope.agentDir !== scope.agentDir ||
|
|
4401
|
+
candidateScope.sessionsRoot !== scope.sessionsRoot
|
|
4402
|
+
)
|
|
4403
|
+
throw new Error("managed_gc_protocol_root_mismatch");
|
|
4404
|
+
managedGcReserveInventoryWork(operation);
|
|
4405
|
+
const scopeIdentity = managedGcProtocolPathIdentity(candidateScope.directoryPath);
|
|
4406
|
+
if (scopeIdentity.dev !== input.scopeIdentity.dev || scopeIdentity.ino !== input.scopeIdentity.ino)
|
|
4407
|
+
throw new Error("managed_gc_protocol_scope_identity_mismatch");
|
|
4408
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
4409
|
+
const store = candidateTrusted
|
|
4410
|
+
? managedGcScopeReader(candidateScope, candidateTrusted)
|
|
4411
|
+
: new ManagedSessionDescendantStore(
|
|
4412
|
+
trusted.root,
|
|
4413
|
+
candidateScope.directoryPath,
|
|
4414
|
+
undefined,
|
|
4415
|
+
trusted.policy,
|
|
4416
|
+
candidateScope.agentDir,
|
|
4417
|
+
{
|
|
4418
|
+
canonicalPath: candidateScope.directoryPath,
|
|
4419
|
+
dev: BigInt(scopeIdentity.dev),
|
|
4420
|
+
ino: BigInt(scopeIdentity.ino),
|
|
4421
|
+
},
|
|
4422
|
+
"read-only",
|
|
4423
|
+
);
|
|
4424
|
+
managedGcWithClosedReader(store, () => {
|
|
4425
|
+
managedGcReserveInventoryWork(operation);
|
|
4426
|
+
store.verifyRootSecurity();
|
|
4427
|
+
const checkedBinding = managedGcProtocolFileSnapshot(candidateScope, store, MANAGED_SESSION_BINDING_FILE);
|
|
4428
|
+
if (!managedGcProtocolFileIdentityMatches(checkedBinding.identity, input.bindingIdentity))
|
|
4429
|
+
throw new Error("managed_gc_protocol_binding_identity_mismatch");
|
|
4430
|
+
const protocolDirectory = managedGcProtocolDirectorySnapshot(
|
|
4431
|
+
candidateScope,
|
|
4432
|
+
store,
|
|
4433
|
+
MANAGED_INTERNAL_DIRECTORY,
|
|
4434
|
+
operation,
|
|
4435
|
+
);
|
|
4436
|
+
if (
|
|
4437
|
+
protocolDirectory.path !== input.protocolIdentity.path ||
|
|
4438
|
+
protocolDirectory.stat.dev.toString() !== input.protocolIdentity.dev ||
|
|
4439
|
+
protocolDirectory.stat.ino.toString() !== input.protocolIdentity.ino ||
|
|
4440
|
+
protocolDirectory.stat.mtimeNs.toString() !== input.protocolIdentity.mtimeNs ||
|
|
4441
|
+
protocolDirectory.stat.ctimeNs.toString() !== input.protocolIdentity.ctimeNs ||
|
|
4442
|
+
Number(protocolDirectory.stat.mode & 0o777n) !== input.protocolIdentity.mode ||
|
|
4443
|
+
protocolDirectory.names.length !== MANAGED_GC_PROTOCOL_ROLES.length ||
|
|
4444
|
+
MANAGED_GC_PROTOCOL_ROLES.some(role => !protocolDirectory.names.includes(role))
|
|
4445
|
+
)
|
|
4446
|
+
throw new Error("managed_gc_protocol_roles_invalid");
|
|
4447
|
+
managedGcReserveInventoryWork(operation, 2);
|
|
4448
|
+
const directories: ManagedGcProtocolDirectorySnapshot[] = [];
|
|
4449
|
+
const receiptHistories = new Map<string, MutableManagedGcReceiptHistory>();
|
|
4450
|
+
for (const role of MANAGED_GC_PROTOCOL_ROLES) {
|
|
4451
|
+
const directory = managedGcProtocolDirectorySnapshot(candidateScope, store, role, operation);
|
|
4452
|
+
managedGcReserveInventoryWork(operation, directory.names.length);
|
|
4453
|
+
const files: ManagedGcProtocolFileSnapshot[] = [];
|
|
4454
|
+
for (const name of directory.names) {
|
|
4455
|
+
if (!name || path.basename(name) !== name || name.includes(".jsonl"))
|
|
4456
|
+
throw new Error(`managed_gc_protocol_entry_invalid:${role}/${name}`);
|
|
4457
|
+
const relative = `${MANAGED_INTERNAL_DIRECTORY}/${role}/${name}`;
|
|
4458
|
+
const cleanupLike = role === MANAGED_TOMBSTONES_DIRECTORY && isManagedGcCleanupLikeName(name);
|
|
4459
|
+
if (cleanupLike && !isManagedGcCleanupReceiptPath(relative))
|
|
4460
|
+
throw new Error("managed_gc_protocol_tombstone_role_invalid");
|
|
4461
|
+
const pendingCleanup =
|
|
4462
|
+
role === MANAGED_TOMBSTONES_DIRECTORY && isManagedGcPendingCleanupReceiptPath(relative);
|
|
4463
|
+
let captured: ReturnType<typeof managedGcProtocolFileSnapshot>;
|
|
4464
|
+
try {
|
|
4465
|
+
if (pendingCleanup)
|
|
4466
|
+
validateManagedGcProtocolTombstoneFile(
|
|
4467
|
+
candidateScope,
|
|
4468
|
+
path.join(directory.path, name),
|
|
4469
|
+
name,
|
|
4470
|
+
new Uint8Array(),
|
|
4471
|
+
journals,
|
|
4472
|
+
);
|
|
4473
|
+
captured = managedGcProtocolFileSnapshot(candidateScope, store, relative, receiptHistories);
|
|
4474
|
+
if (role === MANAGED_LOCKS_DIRECTORY) {
|
|
4475
|
+
if (isManagedLockQuarantineName(name)) {
|
|
4476
|
+
if (
|
|
4477
|
+
captured.snapshot.bytes.byteLength !== 0 ||
|
|
4478
|
+
captured.identity.size !== 0 ||
|
|
4479
|
+
captured.identity.sha256 !== createHash("sha256").digest("hex")
|
|
4480
|
+
)
|
|
4481
|
+
throw new Error("managed_gc_protocol_lock_quarantine_invalid");
|
|
4482
|
+
} else {
|
|
4483
|
+
if (!/^(?:[a-f0-9]{64}|gc-retirement-[a-f0-9]{64})\.lock$/u.test(name))
|
|
4484
|
+
throw new Error("managed_gc_protocol_lock_role_invalid");
|
|
4485
|
+
if (!parseManagedLockRecord(captured.snapshot.bytes))
|
|
4486
|
+
throw new Error("managed_gc_protocol_lock_invalid");
|
|
4487
|
+
}
|
|
4488
|
+
} else if (role === MANAGED_RECEIPTS_DIRECTORY) {
|
|
4489
|
+
if (
|
|
4490
|
+
/^gc-retirement-[a-f0-9]{64}-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.test(
|
|
4491
|
+
name,
|
|
4492
|
+
)
|
|
4493
|
+
) {
|
|
4494
|
+
const key = name.slice(
|
|
4495
|
+
MANAGED_GC_RETIREMENT_PREFIX.length,
|
|
4496
|
+
MANAGED_GC_RETIREMENT_PREFIX.length + 64,
|
|
4497
|
+
);
|
|
4498
|
+
if (
|
|
4499
|
+
!journals.some(
|
|
4500
|
+
item =>
|
|
4501
|
+
item.scope.directoryPath === candidateScope.directoryPath &&
|
|
4502
|
+
managedGcRetirementTranscriptKey(item.receipt.transcriptPath) === key,
|
|
4503
|
+
)
|
|
4504
|
+
)
|
|
4505
|
+
throw new Error("task_artifact_owner_continuation_state_missing");
|
|
4506
|
+
} else {
|
|
4507
|
+
validateManagedGcProtocolMigrationReceipt(
|
|
4508
|
+
candidateScope,
|
|
4509
|
+
path.join(directory.path, name),
|
|
4510
|
+
name,
|
|
4511
|
+
captured.snapshot.bytes,
|
|
4512
|
+
);
|
|
4513
|
+
}
|
|
4514
|
+
} else {
|
|
4515
|
+
if (!pendingCleanup)
|
|
4516
|
+
validateManagedGcProtocolTombstoneFile(
|
|
4517
|
+
candidateScope,
|
|
4518
|
+
path.join(directory.path, name),
|
|
4519
|
+
name,
|
|
4520
|
+
captured.snapshot.bytes,
|
|
4521
|
+
journals,
|
|
4522
|
+
);
|
|
4523
|
+
}
|
|
4524
|
+
} catch (error) {
|
|
4525
|
+
if (isManagedGcJournalCapacityError(error)) throw error;
|
|
4526
|
+
const detail = error instanceof Error ? error.message : "unknown";
|
|
4527
|
+
throw new Error(`${detail} [${role}/${name}]`);
|
|
4528
|
+
}
|
|
4529
|
+
managedGcReserveInventoryWork(operation);
|
|
4530
|
+
files.push(captured.identity);
|
|
4531
|
+
totalFiles++;
|
|
4532
|
+
if (totalFiles > MANAGED_GC_PROTOCOL_MAX_ENTRIES)
|
|
4533
|
+
throw new Error("managed_gc_protocol_entry_limit_exceeded");
|
|
4534
|
+
}
|
|
4535
|
+
const finalDirectory = managedGcProtocolDirectorySnapshot(candidateScope, store, role, operation);
|
|
4536
|
+
if (
|
|
4537
|
+
finalDirectory.stat.dev !== directory.stat.dev ||
|
|
4538
|
+
finalDirectory.stat.ino !== directory.stat.ino ||
|
|
4539
|
+
finalDirectory.stat.mtimeNs !== directory.stat.mtimeNs ||
|
|
4540
|
+
finalDirectory.stat.ctimeNs !== directory.stat.ctimeNs ||
|
|
4541
|
+
finalDirectory.stat.mode !== directory.stat.mode ||
|
|
4542
|
+
finalDirectory.names.length !== directory.names.length ||
|
|
4543
|
+
finalDirectory.names.some((name, index) => name !== directory.names[index])
|
|
4544
|
+
)
|
|
4545
|
+
throw new Error("managed_gc_protocol_directory_changed");
|
|
4546
|
+
managedGcReserveInventoryWork(operation);
|
|
4547
|
+
directories.push({
|
|
4548
|
+
role,
|
|
4549
|
+
path: directory.path,
|
|
4550
|
+
dev: directory.stat.dev.toString(),
|
|
4551
|
+
ino: directory.stat.ino.toString(),
|
|
4552
|
+
mtimeNs: directory.stat.mtimeNs.toString(),
|
|
4553
|
+
ctimeNs: directory.stat.ctimeNs.toString(),
|
|
4554
|
+
mode: Number(directory.stat.mode & 0o777n),
|
|
4555
|
+
files,
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
const finalProtocol = managedGcProtocolDirectorySnapshot(
|
|
4559
|
+
candidateScope,
|
|
4560
|
+
store,
|
|
4561
|
+
MANAGED_INTERNAL_DIRECTORY,
|
|
4562
|
+
operation,
|
|
4563
|
+
);
|
|
4564
|
+
if (
|
|
4565
|
+
finalProtocol.stat.dev !== protocolDirectory.stat.dev ||
|
|
4566
|
+
finalProtocol.stat.ino !== protocolDirectory.stat.ino ||
|
|
4567
|
+
finalProtocol.stat.mtimeNs !== protocolDirectory.stat.mtimeNs ||
|
|
4568
|
+
finalProtocol.stat.ctimeNs !== protocolDirectory.stat.ctimeNs ||
|
|
4569
|
+
finalProtocol.stat.mode !== protocolDirectory.stat.mode ||
|
|
4570
|
+
finalProtocol.names.length !== protocolDirectory.names.length ||
|
|
4571
|
+
finalProtocol.names.some((name, index) => name !== protocolDirectory.names[index])
|
|
4572
|
+
)
|
|
4573
|
+
throw new Error("managed_gc_protocol_tree_changed");
|
|
4574
|
+
managedGcReserveInventoryWork(operation);
|
|
4575
|
+
const comparisonReceiptHistories = new Map<string, MutableManagedGcReceiptHistory>();
|
|
4576
|
+
for (const directory of directories) {
|
|
4577
|
+
const relative = `${MANAGED_INTERNAL_DIRECTORY}/${directory.role}`;
|
|
4578
|
+
const currentDirectory = managedGcProtocolDirectorySnapshot(
|
|
4579
|
+
candidateScope,
|
|
4580
|
+
store,
|
|
4581
|
+
directory.role,
|
|
4582
|
+
operation,
|
|
4583
|
+
);
|
|
4584
|
+
const names = currentDirectory.names;
|
|
4585
|
+
if (
|
|
4586
|
+
currentDirectory.stat.dev !== BigInt(directory.dev) ||
|
|
4587
|
+
currentDirectory.stat.ino !== BigInt(directory.ino) ||
|
|
4588
|
+
currentDirectory.stat.mtimeNs !== BigInt(directory.mtimeNs) ||
|
|
4589
|
+
currentDirectory.stat.ctimeNs !== BigInt(directory.ctimeNs) ||
|
|
4590
|
+
names.length !== directory.files.length ||
|
|
4591
|
+
names.some((name, index) => name !== directory.files[index]?.name)
|
|
4592
|
+
)
|
|
4593
|
+
throw new Error("managed_gc_protocol_directory_changed");
|
|
4594
|
+
for (const expected of directory.files) {
|
|
4595
|
+
const actual = managedGcProtocolFileSnapshot(
|
|
4596
|
+
candidateScope,
|
|
4597
|
+
store,
|
|
4598
|
+
`${relative}/${expected.name}`,
|
|
4599
|
+
comparisonReceiptHistories,
|
|
4600
|
+
);
|
|
4601
|
+
if (!managedGcProtocolFileIdentityMatches(actual.identity, expected))
|
|
4602
|
+
throw new Error("managed_gc_protocol_file_changed");
|
|
4603
|
+
}
|
|
4604
|
+
managedGcReserveInventoryWork(operation);
|
|
4605
|
+
const current = managedGcProtocolDirectoryStat(directory.path);
|
|
4606
|
+
if (
|
|
4607
|
+
current.dev.toString() !== directory.dev ||
|
|
4608
|
+
current.ino.toString() !== directory.ino ||
|
|
4609
|
+
current.mtimeNs.toString() !== directory.mtimeNs ||
|
|
4610
|
+
current.ctimeNs.toString() !== directory.ctimeNs ||
|
|
4611
|
+
Number(current.mode & 0o777n) !== directory.mode
|
|
4612
|
+
)
|
|
4613
|
+
throw new Error("managed_gc_protocol_directory_changed");
|
|
4614
|
+
}
|
|
4615
|
+
const finalBinding = managedGcProtocolFileSnapshot(candidateScope, store, MANAGED_SESSION_BINDING_FILE);
|
|
4616
|
+
if (!managedGcProtocolFileIdentityMatches(finalBinding.identity, input.bindingIdentity))
|
|
4617
|
+
throw new Error("managed_gc_protocol_binding_changed");
|
|
4618
|
+
managedGcReserveInventoryWork(operation);
|
|
4619
|
+
snapshots.push({
|
|
4620
|
+
configuredRoot,
|
|
4621
|
+
profile,
|
|
4622
|
+
sessionsRoot,
|
|
4623
|
+
scope: scopeIdentity,
|
|
4624
|
+
binding: bindingIdentity,
|
|
4625
|
+
protocol: {
|
|
4626
|
+
path: protocolDirectory.path,
|
|
4627
|
+
dev: protocolDirectory.stat.dev.toString(),
|
|
4628
|
+
ino: protocolDirectory.stat.ino.toString(),
|
|
4629
|
+
},
|
|
4630
|
+
protocolMtimeNs: protocolDirectory.stat.mtimeNs.toString(),
|
|
4631
|
+
protocolCtimeNs: protocolDirectory.stat.ctimeNs.toString(),
|
|
4632
|
+
protocolMode: Number(protocolDirectory.stat.mode & 0o777n),
|
|
4633
|
+
directories,
|
|
4634
|
+
});
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
const latestJournals = await discoverManagedGcSessionRetirementReceiptsWithOperation(
|
|
4638
|
+
{ agentDir: scope.agentDir, sessionsRoot: scope.sessionsRoot },
|
|
4639
|
+
operation,
|
|
4640
|
+
);
|
|
4641
|
+
if (!deepSame(latestJournals, journals)) throw new Error("managed_gc_protocol_journal_changed");
|
|
4642
|
+
assertAuthority(operation);
|
|
4643
|
+
return snapshots;
|
|
4644
|
+
};
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
function managedGcReceiptForPublication(
|
|
4648
|
+
scope: ManagedScope,
|
|
4649
|
+
receipt: ManagedGcSessionRetirementReceipt,
|
|
4650
|
+
ownerContext: TaskArtifactOwnerStorageContext,
|
|
4651
|
+
attempt?: number,
|
|
4652
|
+
history?: ManagedGcReceiptHistory,
|
|
4653
|
+
): {
|
|
4654
|
+
readonly target: ManagedGcSessionRetirementTarget;
|
|
4655
|
+
readonly record: Record<string, unknown>;
|
|
4656
|
+
readonly parsed: ManagedGcSessionRetirementReceipt;
|
|
4657
|
+
} {
|
|
4658
|
+
validateManagedGcTranscriptPath(scope, receipt.transcriptPath);
|
|
4659
|
+
if (
|
|
4660
|
+
history &&
|
|
4661
|
+
(history.entryCount >= MANAGED_ARTIFACT_MAX_FILES || history.directoryEntryCount >= MANAGED_ARTIFACT_MAX_FILES)
|
|
4662
|
+
)
|
|
4663
|
+
managedGcJournalCapacity();
|
|
4664
|
+
const candidateRecord = managedGcReceiptRecord(scope, receipt, attempt);
|
|
4665
|
+
const candidateByteLength = managedGcJsonByteLength(candidateRecord);
|
|
4666
|
+
if (history && candidateByteLength > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - history.encodedBytes)
|
|
4667
|
+
managedGcJournalCapacity();
|
|
4668
|
+
const parsed = parseManagedGcRetirementReceipt(
|
|
4669
|
+
candidateRecord,
|
|
4670
|
+
scopeDigest(scope.platform, scope.canonicalCwd),
|
|
4671
|
+
ownerContext,
|
|
4672
|
+
receipt,
|
|
4673
|
+
);
|
|
4674
|
+
const record = managedGcReceiptRecord(scope, parsed, attempt);
|
|
4675
|
+
const encodedByteLength = managedGcJsonByteLength(record);
|
|
4676
|
+
if (encodedByteLength !== candidateByteLength) throw new Error("managed_gc_receipt_encoding_mismatch");
|
|
4677
|
+
return { target: receipt, record, parsed };
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
function publishManagedGcReceiptNoReplace(
|
|
4681
|
+
store: ManagedSessionDescendantStore,
|
|
4682
|
+
lock: ManagedStorageLock,
|
|
4683
|
+
relativePath: string,
|
|
4684
|
+
record: Record<string, unknown>,
|
|
4685
|
+
): void {
|
|
4686
|
+
lock.assertOwned();
|
|
4687
|
+
store.assertBound();
|
|
4688
|
+
const expectedByteLength = managedGcJsonByteLength(record);
|
|
4689
|
+
const serializedJson = JSON.stringify(record, (_key, value: unknown) =>
|
|
4690
|
+
typeof value === "bigint" ? value.toString() : value,
|
|
4691
|
+
);
|
|
4692
|
+
const serialized = `${serializedJson}\n`;
|
|
4693
|
+
const actualByteLength = Buffer.byteLength(serialized, "utf8");
|
|
4694
|
+
if (actualByteLength > MANAGED_GC_RECEIPT_MAX_BYTES) managedGcReceiptCapacity();
|
|
4695
|
+
if (actualByteLength !== expectedByteLength) throw new Error("managed_gc_receipt_encoding_mismatch");
|
|
4696
|
+
const bytes = Buffer.from(serialized, "utf8");
|
|
4697
|
+
try {
|
|
4698
|
+
store.publishNoReplaceSync(relativePath, bytes);
|
|
4699
|
+
} catch (error) {
|
|
4700
|
+
if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
|
|
4701
|
+
const existing = readManagedGcReceiptSnapshot(store, relativePath);
|
|
4702
|
+
if (!existing?.bytes.equals(bytes)) throw error;
|
|
4703
|
+
}
|
|
4704
|
+
lock.assertOwned();
|
|
4705
|
+
const persisted = readManagedGcReceiptSnapshot(store, relativePath);
|
|
4706
|
+
if (!persisted?.bytes.equals(bytes)) throw new Error("durability_failed");
|
|
4707
|
+
store.assertBound();
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4710
|
+
/** Atomically publish one journal state under the managed scope lock; no GC effects are activated here. */
|
|
4711
|
+
export async function publishManagedGcSessionRetirementReceipt(
|
|
4712
|
+
scope: ManagedScope,
|
|
4713
|
+
receipt: ManagedGcSessionRetirementReceipt,
|
|
4714
|
+
): Promise<ManagedGcSessionRetirementReceipt> {
|
|
4715
|
+
validateManagedGcTranscriptPath(scope, receipt.transcriptPath);
|
|
4716
|
+
return withManagedGcRetirementJournal(scope, receipt.transcriptPath, (trusted, store, lock) => {
|
|
4717
|
+
const operation: ManagedGcInventoryOperation = { kind: "leased-journal", lock };
|
|
4718
|
+
const currentRead = readManagedGcSessionRetirementReceiptUnlocked(
|
|
4719
|
+
scope,
|
|
4720
|
+
receipt.transcriptPath,
|
|
4721
|
+
trusted,
|
|
4722
|
+
store,
|
|
4723
|
+
operation,
|
|
4724
|
+
);
|
|
4725
|
+
const current = currentRead.receipt;
|
|
4726
|
+
const target = receipt;
|
|
4727
|
+
let attempt: number | undefined;
|
|
4728
|
+
if (!current) {
|
|
4729
|
+
if (receipt.state !== "prepared") throw new Error("task_artifact_owner_continuation_state_order_invalid");
|
|
4730
|
+
assertManagedGcTranscriptUnchangedOrAbsent(scope, target, false);
|
|
4731
|
+
const capturedEvidence = captureTaskArtifactOwnerDeletionEvidence(
|
|
4732
|
+
trusted.ownerContext,
|
|
4733
|
+
target.sessionId,
|
|
4734
|
+
target.taskArtifactOwnerLocator,
|
|
4735
|
+
);
|
|
4736
|
+
if (
|
|
4737
|
+
!capturedEvidence ||
|
|
4738
|
+
!deepSame(
|
|
4739
|
+
capturedEvidence,
|
|
4740
|
+
parseTaskArtifactOwnerDeletionEvidence(receipt.taskArtifactOwnerDeletionEvidence),
|
|
4741
|
+
)
|
|
4742
|
+
)
|
|
4743
|
+
throw new Error("task_artifact_owner_continuation_evidence_mismatch");
|
|
4744
|
+
} else {
|
|
4745
|
+
const prepared = currentRead.prepared;
|
|
4746
|
+
if (!prepared) throw new Error("task_artifact_owner_continuation_state_missing");
|
|
4747
|
+
assertSameManagedGcImmutableAuthority(prepared, receipt);
|
|
4748
|
+
assertManagedGcTranscriptUnchangedOrAbsent(scope, prepared, true);
|
|
4749
|
+
if (receipt.state === "prepared") {
|
|
4750
|
+
managedGcReceiptForPublication(scope, receipt, trusted.ownerContext);
|
|
4751
|
+
return current;
|
|
4752
|
+
}
|
|
4753
|
+
if (receipt.state === "artifacts_removed" && current.state !== "prepared") {
|
|
4754
|
+
managedGcReceiptForPublication(scope, receipt, trusted.ownerContext);
|
|
4755
|
+
return current;
|
|
4756
|
+
}
|
|
4757
|
+
if (receipt.state === "owner_pending") {
|
|
4758
|
+
if (current.state !== "artifacts_removed" && current.state !== "owner_pending")
|
|
4759
|
+
throw new Error("task_artifact_owner_continuation_state_order_invalid");
|
|
4760
|
+
attempt = current.state === "owner_pending" ? (current.ownerRetirementAttempt ?? 0) + 1 : 1;
|
|
4761
|
+
if (current.state === "owner_pending") {
|
|
4762
|
+
const previous = current.taskArtifactOwnerRetirementOutcome;
|
|
4763
|
+
const next = receipt.taskArtifactOwnerRetirementOutcome;
|
|
4764
|
+
if (
|
|
4765
|
+
!previous ||
|
|
4766
|
+
previous.kind === "completed" ||
|
|
4767
|
+
!next ||
|
|
4768
|
+
next.kind === "completed" ||
|
|
4769
|
+
!continuationExtendsPrevious(previous.continuation, next.continuation)
|
|
4770
|
+
)
|
|
4771
|
+
throw new Error("task_artifact_owner_continuation_authority_mismatch");
|
|
4772
|
+
}
|
|
4773
|
+
} else if (receipt.state === "owner_retired") {
|
|
4774
|
+
if (current.state === "owner_retired") {
|
|
4775
|
+
const existing = current.taskArtifactOwnerRetirementOutcome;
|
|
4776
|
+
const candidate = managedGcReceiptForPublication(scope, receipt, trusted.ownerContext).parsed;
|
|
4777
|
+
if (!existing || !deepSame(existing, candidate.taskArtifactOwnerRetirementOutcome))
|
|
4778
|
+
throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
4779
|
+
return current;
|
|
4780
|
+
}
|
|
4781
|
+
if (current.state !== "artifacts_removed" && current.state !== "owner_pending")
|
|
4782
|
+
throw new Error("task_artifact_owner_continuation_state_order_invalid");
|
|
4783
|
+
} else if (receipt.state !== "artifacts_removed") {
|
|
4784
|
+
throw new Error("task_artifact_owner_continuation_state_order_invalid");
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
assertManagedGcReceiptHistoryDirectoryUnchanged(scope, store, currentRead.history, operation);
|
|
4788
|
+
const { record, parsed } = managedGcReceiptForPublication(
|
|
4789
|
+
scope,
|
|
4790
|
+
receipt,
|
|
4791
|
+
trusted.ownerContext,
|
|
4792
|
+
attempt,
|
|
4793
|
+
currentRead.history,
|
|
4794
|
+
);
|
|
4795
|
+
if (receipt.state === "owner_retired") {
|
|
4796
|
+
const outcome = parsed.taskArtifactOwnerRetirementOutcome;
|
|
4797
|
+
if (outcome?.kind !== "completed") throw new Error("task_artifact_owner_continuation_outcome_mismatch");
|
|
4798
|
+
verifyTaskArtifactOwnerPhysicalRetirement(
|
|
4799
|
+
trusted.ownerContext,
|
|
4800
|
+
parsed.taskArtifactOwnerDeletionEvidence,
|
|
4801
|
+
outcome,
|
|
4802
|
+
);
|
|
4803
|
+
}
|
|
4804
|
+
const relativePath = managedGcRetirementReceiptRelativePath(receipt.transcriptPath, receipt.state, attempt);
|
|
4805
|
+
publishManagedGcReceiptNoReplace(store, lock, relativePath, record);
|
|
4806
|
+
const persistedRead = readManagedGcSessionRetirementReceiptUnlocked(
|
|
4807
|
+
scope,
|
|
4808
|
+
receipt.transcriptPath,
|
|
4809
|
+
trusted,
|
|
4810
|
+
store,
|
|
4811
|
+
operation,
|
|
4812
|
+
);
|
|
4813
|
+
const persisted = persistedRead.receipt;
|
|
4814
|
+
if (!persisted) throw new Error("durability_failed");
|
|
4815
|
+
const expectedState = receipt.state;
|
|
4816
|
+
if (
|
|
4817
|
+
persisted.state !== expectedState ||
|
|
4818
|
+
(expectedState === "owner_pending" && persisted.ownerRetirementAttempt !== attempt) ||
|
|
4819
|
+
!deepSame(persisted.taskArtifactOwnerDeletionEvidence, parsed.taskArtifactOwnerDeletionEvidence)
|
|
4820
|
+
)
|
|
4821
|
+
throw new Error("durability_failed");
|
|
4822
|
+
return persisted;
|
|
4823
|
+
});
|
|
4824
|
+
}
|
|
4825
|
+
function retiredTargets(scope: ManagedScope, pathname: string): readonly RetiredTarget[] | undefined {
|
|
4826
|
+
let value: unknown;
|
|
4827
|
+
try {
|
|
4828
|
+
value = JSON.parse(captureManagedFileNoFollow(pathname).bytes.toString("utf8"));
|
|
4829
|
+
const targets = parseRetiredTargetRecord(scope, value);
|
|
4830
|
+
if (!targets) assertLegacyOwnerFreeRecord(value);
|
|
4831
|
+
return targets;
|
|
4832
|
+
} catch (error) {
|
|
4833
|
+
assertLegacyOwnerFreeRecord(value);
|
|
4834
|
+
if (isOwnerBoundaryFailure(error)) throw error;
|
|
4835
|
+
if (error instanceof SyntaxError) throw new Error("durability_failed", { cause: error });
|
|
4836
|
+
return undefined;
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
const OWNER_CONSUMER_UNAVAILABLE = "task_artifact_owner_legacy_scope_unsupported";
|
|
4841
|
+
|
|
4842
|
+
function isOwnerBoundaryFailure(error: unknown): boolean {
|
|
4843
|
+
return error instanceof Error && error.message.startsWith("task_artifact_owner_");
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4846
|
+
function hasOwnerClaims(value: unknown): boolean {
|
|
4847
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
4848
|
+
const record = value as Record<string, unknown>;
|
|
4849
|
+
return (
|
|
4850
|
+
Object.keys(record).some(key => key.startsWith("taskArtifactOwner")) ||
|
|
4851
|
+
(record.target !== undefined && hasOwnerClaims(record.target)) ||
|
|
4852
|
+
(Array.isArray(record.targets) && record.targets.some(hasOwnerClaims))
|
|
4853
|
+
);
|
|
4854
|
+
}
|
|
4855
|
+
|
|
4856
|
+
function assertLegacyOwnerFreeRecord(value: unknown): void {
|
|
4857
|
+
if (hasOwnerClaims(value)) throw new Error(OWNER_CONSUMER_UNAVAILABLE);
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
function assertOwnerTranscriptLocator(
|
|
4861
|
+
target: Pick<
|
|
4862
|
+
VerifiedSessionDeleteTarget,
|
|
4863
|
+
"transcriptPath" | "sessionId" | "transcriptIdentity" | "detachedTranscriptPath"
|
|
4864
|
+
>,
|
|
4865
|
+
evidence?: TaskArtifactOwnerDeletionEvidence,
|
|
4866
|
+
): void {
|
|
4867
|
+
const pathname = target.detachedTranscriptPath ?? target.transcriptPath;
|
|
4868
|
+
let snapshot: SessionStorageSnapshot;
|
|
4869
|
+
try {
|
|
4870
|
+
snapshot = new FileSessionStorage().readSnapshotSync(pathname);
|
|
4871
|
+
} catch (error) {
|
|
4872
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
4873
|
+
throw error;
|
|
4874
|
+
}
|
|
4875
|
+
// Descriptor-bound owner classification is data, not authorization; storage retains its exact identity fence.
|
|
4876
|
+
const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, target.sessionId);
|
|
4877
|
+
if ((locator && !evidence) || (evidence && !locator)) throw new Error("task_artifact_owner_locator_missing");
|
|
4878
|
+
if (locator && !deepSame(locator, evidence?.locator))
|
|
4879
|
+
throw new Error("task_artifact_owner_continuation_evidence_mismatch");
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4882
|
+
function parseRetiredTargetRecord(scope: ManagedScope, value: unknown): readonly RetiredTarget[] | undefined {
|
|
4883
|
+
if (!value || typeof value !== "object") return undefined;
|
|
4884
|
+
if (Object.keys(value).some(key => key.startsWith("taskArtifactOwner"))) throw new Error(OWNER_CONSUMER_UNAVAILABLE);
|
|
4885
|
+
const record = value as { schemaVersion?: unknown; state?: unknown; scope?: unknown; targets?: unknown };
|
|
4886
|
+
if (
|
|
4887
|
+
record.schemaVersion !== 2 ||
|
|
4888
|
+
record.state !== "retired" ||
|
|
4889
|
+
record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
|
|
4890
|
+
!Array.isArray(record.targets)
|
|
4891
|
+
)
|
|
4892
|
+
return undefined;
|
|
4893
|
+
const targets: RetiredTarget[] = [];
|
|
4894
|
+
for (const target of record.targets) {
|
|
4895
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) return undefined;
|
|
4896
|
+
const item = target as Record<string, unknown>;
|
|
4897
|
+
if (
|
|
4898
|
+
Object.keys(item).some(
|
|
4899
|
+
key => key.startsWith("taskArtifactOwner") && key !== "taskArtifactOwnerDeletionEvidence",
|
|
4900
|
+
)
|
|
4901
|
+
)
|
|
4902
|
+
throw new Error(OWNER_CONSUMER_UNAVAILABLE);
|
|
4903
|
+
const identity = item.identity;
|
|
4904
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity)) return undefined;
|
|
4905
|
+
const fields = identity as Record<string, unknown>;
|
|
4906
|
+
if (
|
|
4907
|
+
typeof item.path !== "string" ||
|
|
4908
|
+
typeof item.sessionId !== "string" ||
|
|
4909
|
+
typeof item.cwd !== "string" ||
|
|
4910
|
+
!pathIsWithin(scope.sessionsRoot, item.path) ||
|
|
4911
|
+
(item.provenance !== undefined && item.provenance !== "v2" && item.provenance !== "legacy") ||
|
|
4912
|
+
typeof fields.canonicalPath !== "string" ||
|
|
4913
|
+
typeof fields.dev !== "string" ||
|
|
4914
|
+
typeof fields.ino !== "string" ||
|
|
4915
|
+
typeof fields.size !== "number" ||
|
|
4916
|
+
typeof fields.mtimeMs !== "number" ||
|
|
4917
|
+
typeof fields.mtimeNs !== "string" ||
|
|
4918
|
+
typeof fields.sha256 !== "string"
|
|
4919
|
+
)
|
|
4920
|
+
return undefined;
|
|
4921
|
+
const provenance =
|
|
4922
|
+
item.provenance === "v2" || item.provenance === "legacy"
|
|
4923
|
+
? item.provenance
|
|
4924
|
+
: path.dirname(item.path) === scope.directoryPath
|
|
4925
|
+
? "v2"
|
|
4926
|
+
: "legacy";
|
|
4927
|
+
let taskArtifactOwnerDeletionEvidence: TaskArtifactOwnerDeletionEvidence | undefined;
|
|
4928
|
+
if (Object.hasOwn(item, "taskArtifactOwnerDeletionEvidence")) {
|
|
4929
|
+
try {
|
|
4930
|
+
taskArtifactOwnerDeletionEvidence = parseTaskArtifactOwnerDeletionEvidence(
|
|
4931
|
+
item.taskArtifactOwnerDeletionEvidence,
|
|
4932
|
+
);
|
|
4933
|
+
if (taskArtifactOwnerDeletionEvidence.sessionId !== item.sessionId)
|
|
4934
|
+
throw new Error("owner_session_mismatch");
|
|
4935
|
+
} catch (cause) {
|
|
4936
|
+
throw new Error("task_artifact_owner_legacy_scope_unsupported", { cause });
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
targets.push({
|
|
4940
|
+
...(taskArtifactOwnerDeletionEvidence ? { taskArtifactOwnerDeletionEvidence } : {}),
|
|
4941
|
+
path: item.path,
|
|
4942
|
+
sessionId: item.sessionId,
|
|
4943
|
+
cwd: item.cwd,
|
|
4944
|
+
provenance,
|
|
4945
|
+
migrationState: provenance === "v2" ? "native_v2" : "legacy_unmigrated",
|
|
4946
|
+
identity: {
|
|
4947
|
+
canonicalPath: fields.canonicalPath,
|
|
4948
|
+
dev: BigInt(fields.dev),
|
|
4949
|
+
ino: BigInt(fields.ino),
|
|
4950
|
+
size: fields.size,
|
|
4951
|
+
mtimeMs: fields.mtimeMs,
|
|
4952
|
+
mtimeNs: BigInt(fields.mtimeNs),
|
|
4953
|
+
sha256: fields.sha256,
|
|
4954
|
+
sessionId: item.sessionId,
|
|
4955
|
+
},
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4958
|
+
return targets;
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
/**
|
|
4962
|
+
* Append-only cleanup state machine:
|
|
4963
|
+
* `pending(N)` durably authorizes only its planned quarantine names before detach;
|
|
4964
|
+
* a returned partial result appends `pending(N + 1)` with the observed detached
|
|
4965
|
+
* identity/path. Restart accepts only a contiguous, target-bound sequence.
|
|
4966
|
+
*/
|
|
4967
|
+
type RetainedArtifactsRootReceipt = {
|
|
4968
|
+
path: string;
|
|
4969
|
+
identity: SessionStorageFileIdentity;
|
|
4970
|
+
tree: NativeDirectoryTreeSnapshot;
|
|
4971
|
+
};
|
|
4972
|
+
|
|
4973
|
+
function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
|
|
4974
|
+
const pathname = receipt.detachedArtifactsPath ?? deterministicRemovalRoot(receipt.plannedArtifactsPath);
|
|
4975
|
+
if (!fs.existsSync(pathname)) return undefined;
|
|
4976
|
+
if (!isQuarantinePath(receipt.target, pathname) || !receipt.expectedArtifactsIdentity)
|
|
4977
|
+
throw new Error("durability_failed");
|
|
4978
|
+
const identity = artifactIdentityAt(pathname);
|
|
4979
|
+
if (
|
|
4980
|
+
!identity ||
|
|
4981
|
+
identity.dev !== receipt.expectedArtifactsIdentity.dev ||
|
|
4982
|
+
identity.ino !== receipt.expectedArtifactsIdentity.ino
|
|
4983
|
+
)
|
|
4984
|
+
throw new Error("durability_failed");
|
|
4985
|
+
const tree = snapshotArtifactTree(pathname);
|
|
4986
|
+
if (!artifactTreePayloadAbsent(tree)) throw new Error("durability_failed");
|
|
4987
|
+
return { path: pathname, identity, tree };
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4990
|
+
function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean {
|
|
4991
|
+
if (record.retainedArtifactsRoot === undefined) return true;
|
|
4992
|
+
if (!record.retainedArtifactsRoot || typeof record.retainedArtifactsRoot !== "object")
|
|
4993
|
+
throw new Error("durability_failed");
|
|
4994
|
+
const retained = record.retainedArtifactsRoot as Record<string, unknown>;
|
|
4995
|
+
const identity = retained.identity;
|
|
4996
|
+
const tree = artifactTreeSnapshot(retained.tree);
|
|
4997
|
+
const expectedRetainedPath =
|
|
4998
|
+
typeof record.detachedArtifactsPath === "string"
|
|
4999
|
+
? record.detachedArtifactsPath
|
|
5000
|
+
: typeof record.plannedArtifactsPath === "string"
|
|
5001
|
+
? deterministicRemovalRoot(record.plannedArtifactsPath)
|
|
5002
|
+
: undefined;
|
|
5003
|
+
if (
|
|
5004
|
+
!expectedRetainedPath ||
|
|
5005
|
+
retained.path !== expectedRetainedPath ||
|
|
5006
|
+
record.artifactsPayloadDurable !== true ||
|
|
5007
|
+
!identity ||
|
|
5008
|
+
typeof identity !== "object" ||
|
|
5009
|
+
Array.isArray(identity) ||
|
|
5010
|
+
!tree ||
|
|
5011
|
+
!artifactTreePayloadAbsent(tree)
|
|
5012
|
+
)
|
|
5013
|
+
throw new Error("durability_failed");
|
|
5014
|
+
const artifactIdentity = identity as Record<string, unknown>;
|
|
5015
|
+
if (
|
|
5016
|
+
typeof artifactIdentity.dev !== "string" ||
|
|
5017
|
+
typeof artifactIdentity.ino !== "string" ||
|
|
5018
|
+
typeof artifactIdentity.size !== "number" ||
|
|
5019
|
+
typeof artifactIdentity.mtimeNs !== "string" ||
|
|
5020
|
+
typeof artifactIdentity.sha256 !== "string"
|
|
5021
|
+
)
|
|
5022
|
+
throw new Error("durability_failed");
|
|
5023
|
+
if (!fs.existsSync(retained.path)) return true;
|
|
5024
|
+
const observed = artifactIdentityAt(retained.path);
|
|
5025
|
+
if (
|
|
5026
|
+
!observed ||
|
|
5027
|
+
observed.dev !== BigInt(artifactIdentity.dev) ||
|
|
5028
|
+
observed.ino !== BigInt(artifactIdentity.ino) ||
|
|
5029
|
+
!artifactTreePayloadAbsent(snapshotArtifactTree(retained.path))
|
|
5030
|
+
)
|
|
5031
|
+
throw new Error("durability_failed");
|
|
5032
|
+
return true;
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
type CleanupReceipt = {
|
|
5036
|
+
attempt: number;
|
|
5037
|
+
target: RetiredTarget;
|
|
5038
|
+
taskArtifactOwnerTranscriptDeleted?: true;
|
|
5039
|
+
expectedArtifactsIdentity?: SessionStorageFileIdentity;
|
|
5040
|
+
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
5041
|
+
artifactsPayloadDurable?: true;
|
|
5042
|
+
artifactsRemovedAttempt?: number;
|
|
5043
|
+
detachedArtifactsPath?: string;
|
|
5044
|
+
detachedTranscriptPath?: string;
|
|
5045
|
+
transcriptPayloadDurable?: true;
|
|
5046
|
+
retainedArtifactsSuccessorPath?: string;
|
|
5047
|
+
retainedArtifactsPlaceholderPath?: string;
|
|
5048
|
+
retainedArtifactsUnknownPath?: string;
|
|
5049
|
+
retainedTranscriptSuccessorPath?: string;
|
|
5050
|
+
retainedTranscriptPlaceholderPath?: string;
|
|
5051
|
+
retainedTranscriptUnknownPath?: string;
|
|
5052
|
+
|
|
5053
|
+
plannedArtifactsPath: string;
|
|
5054
|
+
plannedTranscriptPath: string;
|
|
5055
|
+
};
|
|
5056
|
+
|
|
5057
|
+
function cleanupReceiptPath(
|
|
5058
|
+
tombstone: string,
|
|
5059
|
+
target: RetiredTarget,
|
|
5060
|
+
state: "pending" | "artifacts_removed" | "completed",
|
|
5061
|
+
attempt: number,
|
|
5062
|
+
): string {
|
|
5063
|
+
return path.join(
|
|
5064
|
+
path.dirname(tombstone),
|
|
5065
|
+
`${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-${state}-${attempt}.json`,
|
|
5066
|
+
);
|
|
5067
|
+
}
|
|
5068
|
+
|
|
5069
|
+
function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: CleanupReceipt): Record<string, unknown> {
|
|
5070
|
+
return {
|
|
5071
|
+
schemaVersion: 2,
|
|
5072
|
+
state: "cleanup_pending",
|
|
5073
|
+
scope: scopeDigest(scope.platform, scope.canonicalCwd),
|
|
5074
|
+
tombstone,
|
|
5075
|
+
attempt: receipt.attempt,
|
|
5076
|
+
target: {
|
|
5077
|
+
path: receipt.target.path,
|
|
5078
|
+
sessionId: receipt.target.sessionId,
|
|
5079
|
+
cwd: receipt.target.cwd,
|
|
5080
|
+
identity: receipt.target.identity,
|
|
5081
|
+
},
|
|
5082
|
+
...(receipt.taskArtifactOwnerTranscriptDeleted ? { taskArtifactOwnerTranscriptDeleted: true } : {}),
|
|
5083
|
+
...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
|
|
5084
|
+
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
5085
|
+
...(receipt.artifactsPayloadDurable === true ? { artifactsPayloadDurable: true } : {}),
|
|
5086
|
+
...(receipt.artifactsRemovedAttempt !== undefined
|
|
5087
|
+
? { artifactsRemovedAttempt: receipt.artifactsRemovedAttempt }
|
|
5088
|
+
: {}),
|
|
5089
|
+
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
5090
|
+
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
5091
|
+
...(receipt.transcriptPayloadDurable === true ? { transcriptPayloadDurable: true } : {}),
|
|
5092
|
+
...(receipt.retainedArtifactsSuccessorPath
|
|
5093
|
+
? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
|
|
5094
|
+
: {}),
|
|
5095
|
+
...(receipt.retainedArtifactsPlaceholderPath
|
|
5096
|
+
? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
|
|
5097
|
+
: {}),
|
|
5098
|
+
...(receipt.retainedArtifactsUnknownPath
|
|
5099
|
+
? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
|
|
5100
|
+
: {}),
|
|
5101
|
+
...(receipt.retainedTranscriptSuccessorPath
|
|
5102
|
+
? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
|
|
5103
|
+
: {}),
|
|
5104
|
+
...(receipt.retainedTranscriptPlaceholderPath
|
|
5105
|
+
? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
|
|
5106
|
+
: {}),
|
|
5107
|
+
...(receipt.retainedTranscriptUnknownPath
|
|
5108
|
+
? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
|
|
5109
|
+
: {}),
|
|
5110
|
+
plannedArtifactsPath: receipt.plannedArtifactsPath,
|
|
5111
|
+
plannedTranscriptPath: receipt.plannedTranscriptPath,
|
|
5112
|
+
};
|
|
5113
|
+
}
|
|
5114
|
+
|
|
5115
|
+
type CleanupArtifactsRemovedEvidence = { retainedArtifactsRootPath?: string };
|
|
5116
|
+
|
|
5117
|
+
function cleanupArtifactsRemovedEvidence(
|
|
5118
|
+
scope: ManagedScope,
|
|
5119
|
+
tombstone: string,
|
|
5120
|
+
target: RetiredTarget,
|
|
5121
|
+
attempt: number,
|
|
5122
|
+
): CleanupArtifactsRemovedEvidence | undefined {
|
|
5123
|
+
try {
|
|
5124
|
+
const value: unknown = JSON.parse(
|
|
5125
|
+
captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "artifacts_removed", attempt)).bytes.toString(
|
|
2308
5126
|
"utf8",
|
|
2309
5127
|
),
|
|
2310
5128
|
);
|
|
@@ -2875,133 +5693,231 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
2875
5693
|
return snapshot as unknown as NativeDirectoryTreeSnapshot;
|
|
2876
5694
|
}
|
|
2877
5695
|
|
|
5696
|
+
const CLEANUP_RECEIPT_REPLAY_MAX_BYTES = Math.min(
|
|
5697
|
+
MANAGED_ARTIFACT_MAX_FILE_BYTES,
|
|
5698
|
+
MANAGED_SESSION_READ_RANGE_MAX_BYTES,
|
|
5699
|
+
);
|
|
5700
|
+
const CLEANUP_RECEIPT_REPLAY_MAX_WORK = MANAGED_ARTIFACT_MAX_FILES * 2;
|
|
5701
|
+
const CLEANUP_RECEIPT_REPLAY_CHUNK_BYTES = 64 * 1024;
|
|
5702
|
+
|
|
5703
|
+
interface CleanupReceiptReplayBudget {
|
|
5704
|
+
workUnits: number;
|
|
5705
|
+
directoryEntries: number;
|
|
5706
|
+
receiptReads: number;
|
|
5707
|
+
readBytes: number;
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
function cleanupReceiptReplayCapacity(): never {
|
|
5711
|
+
throw new Error("managed_gc_journal_capacity_exceeded");
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5714
|
+
function chargeCleanupReceiptReplayWork(budget: CleanupReceiptReplayBudget, amount = 1): void {
|
|
5715
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || budget.workUnits > CLEANUP_RECEIPT_REPLAY_MAX_WORK - amount)
|
|
5716
|
+
cleanupReceiptReplayCapacity();
|
|
5717
|
+
budget.workUnits += amount;
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
function admitCleanupReceiptReplayEntry(budget: CleanupReceiptReplayBudget): void {
|
|
5721
|
+
if (budget.directoryEntries >= MANAGED_ARTIFACT_MAX_FILES) cleanupReceiptReplayCapacity();
|
|
5722
|
+
chargeCleanupReceiptReplayWork(budget);
|
|
5723
|
+
budget.directoryEntries++;
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
function admitCleanupReceiptReplayRead(budget: CleanupReceiptReplayBudget, size: number): void {
|
|
5727
|
+
if (
|
|
5728
|
+
!Number.isSafeInteger(size) ||
|
|
5729
|
+
size < 0 ||
|
|
5730
|
+
size > CLEANUP_RECEIPT_REPLAY_MAX_BYTES ||
|
|
5731
|
+
budget.receiptReads >= MANAGED_ARTIFACT_MAX_FILES ||
|
|
5732
|
+
budget.readBytes > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - size
|
|
5733
|
+
)
|
|
5734
|
+
cleanupReceiptReplayCapacity();
|
|
5735
|
+
chargeCleanupReceiptReplayWork(budget, Math.max(1, Math.ceil(size / CLEANUP_RECEIPT_REPLAY_CHUNK_BYTES)));
|
|
5736
|
+
budget.receiptReads++;
|
|
5737
|
+
budget.readBytes += size;
|
|
5738
|
+
}
|
|
5739
|
+
|
|
2878
5740
|
function pendingCleanupReceipt(
|
|
2879
5741
|
scope: ManagedScope,
|
|
2880
5742
|
tombstone: string,
|
|
2881
5743
|
target: RetiredTarget,
|
|
2882
5744
|
): CleanupReceipt | undefined {
|
|
5745
|
+
const budget: CleanupReceiptReplayBudget = { workUnits: 0, directoryEntries: 0, receiptReads: 0, readBytes: 0 };
|
|
2883
5746
|
try {
|
|
2884
5747
|
const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
|
|
2885
|
-
const
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
5748
|
+
const directory = fs.opendirSync(path.dirname(tombstone));
|
|
5749
|
+
const records: Array<{ readonly name: string; readonly attempt: number }> = [];
|
|
5750
|
+
try {
|
|
5751
|
+
for (;;) {
|
|
5752
|
+
const entry = directory.readSync();
|
|
5753
|
+
if (!entry) break;
|
|
5754
|
+
admitCleanupReceiptReplayEntry(budget);
|
|
5755
|
+
if (!entry.name.startsWith(prefix) || !entry.name.endsWith(".json")) continue;
|
|
5756
|
+
const suffix = entry.name.slice(prefix.length, -".json".length);
|
|
5757
|
+
if (!/^[1-9][0-9]*$/u.test(suffix)) throw new Error("durability_failed");
|
|
5758
|
+
const attempt = Number(suffix);
|
|
5759
|
+
if (!Number.isSafeInteger(attempt) || String(attempt) !== suffix) throw new Error("durability_failed");
|
|
5760
|
+
if (records.length >= MANAGED_ARTIFACT_MAX_FILES) cleanupReceiptReplayCapacity();
|
|
5761
|
+
records.push({ name: entry.name, attempt });
|
|
5762
|
+
}
|
|
5763
|
+
} finally {
|
|
5764
|
+
directory.closeSync();
|
|
5765
|
+
}
|
|
5766
|
+
records.sort((left, right) => left.attempt - right.attempt);
|
|
2898
5767
|
let latest: CleanupReceipt | undefined;
|
|
2899
5768
|
const plannedPaths = new Set<string>();
|
|
2900
|
-
for (const
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
5769
|
+
for (const { name, attempt } of records) {
|
|
5770
|
+
let snapshot: ManagedFileSnapshot;
|
|
5771
|
+
try {
|
|
5772
|
+
snapshot = captureManagedFileNoFollowBounded(
|
|
5773
|
+
path.join(path.dirname(tombstone), name),
|
|
5774
|
+
CLEANUP_RECEIPT_REPLAY_MAX_BYTES,
|
|
5775
|
+
size => admitCleanupReceiptReplayRead(budget, size),
|
|
5776
|
+
);
|
|
5777
|
+
} catch (error) {
|
|
5778
|
+
if (error instanceof Error && error.message === "artifact_capacity_exceeded")
|
|
5779
|
+
cleanupReceiptReplayCapacity();
|
|
5780
|
+
throw error;
|
|
5781
|
+
}
|
|
5782
|
+
const value: unknown = JSON.parse(snapshot.bytes.toString("utf8"));
|
|
5783
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("durability_failed");
|
|
5784
|
+
latest = parseCleanupReceiptRecord(
|
|
5785
|
+
scope,
|
|
5786
|
+
tombstone,
|
|
5787
|
+
target,
|
|
5788
|
+
value as Record<string, unknown>,
|
|
5789
|
+
attempt,
|
|
5790
|
+
latest,
|
|
5791
|
+
plannedPaths,
|
|
5792
|
+
);
|
|
5793
|
+
}
|
|
5794
|
+
return latest;
|
|
5795
|
+
} catch (error) {
|
|
5796
|
+
if (
|
|
5797
|
+
(error as Error).message === "durability_failed" ||
|
|
5798
|
+
(error as Error).message === "managed_gc_journal_capacity_exceeded" ||
|
|
5799
|
+
isOwnerBoundaryFailure(error)
|
|
5800
|
+
)
|
|
5801
|
+
throw error;
|
|
5802
|
+
return undefined;
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
function parseCleanupReceiptRecord(
|
|
5807
|
+
scope: ManagedScope,
|
|
5808
|
+
tombstone: string,
|
|
5809
|
+
target: RetiredTarget,
|
|
5810
|
+
record: Record<string, unknown>,
|
|
5811
|
+
filenameAttempt: number,
|
|
5812
|
+
latest: CleanupReceipt | undefined,
|
|
5813
|
+
plannedPaths: Set<string>,
|
|
5814
|
+
): CleanupReceipt {
|
|
5815
|
+
if (
|
|
5816
|
+
Object.keys(record).some(
|
|
5817
|
+
key => key.startsWith("taskArtifactOwner") && key !== "taskArtifactOwnerTranscriptDeleted",
|
|
5818
|
+
)
|
|
5819
|
+
)
|
|
5820
|
+
throw new Error(OWNER_CONSUMER_UNAVAILABLE);
|
|
5821
|
+
const attempt = record.attempt;
|
|
5822
|
+
const recorded = record.target as Record<string, unknown> | undefined;
|
|
5823
|
+
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
5824
|
+
if (
|
|
5825
|
+
record.schemaVersion !== 2 ||
|
|
5826
|
+
record.state !== "cleanup_pending" ||
|
|
5827
|
+
record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
|
|
5828
|
+
record.tombstone !== tombstone ||
|
|
5829
|
+
typeof attempt !== "number" ||
|
|
5830
|
+
!Number.isSafeInteger(attempt) ||
|
|
5831
|
+
attempt !== filenameAttempt ||
|
|
5832
|
+
attempt !== (latest?.attempt ?? 0) + 1 ||
|
|
5833
|
+
recorded?.path !== target.path ||
|
|
5834
|
+
recorded.sessionId !== target.sessionId ||
|
|
5835
|
+
recorded.cwd !== target.cwd ||
|
|
5836
|
+
identity?.dev !== String(target.identity.dev) ||
|
|
5837
|
+
identity.ino !== String(target.identity.ino) ||
|
|
5838
|
+
identity.size !== target.identity.size ||
|
|
5839
|
+
identity.mtimeNs !== String(target.identity.mtimeNs) ||
|
|
5840
|
+
identity.sha256 !== target.identity.sha256 ||
|
|
5841
|
+
!isQuarantinePath(target, record.plannedArtifactsPath) ||
|
|
5842
|
+
!isQuarantinePath(target, record.plannedTranscriptPath) ||
|
|
5843
|
+
record.plannedArtifactsPath === record.plannedTranscriptPath ||
|
|
5844
|
+
plannedPaths.has(record.plannedArtifactsPath as string) ||
|
|
5845
|
+
plannedPaths.has(record.plannedTranscriptPath as string) ||
|
|
5846
|
+
(record.detachedArtifactsPath !== undefined && !isQuarantinePath(target, record.detachedArtifactsPath)) ||
|
|
5847
|
+
(record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath)) ||
|
|
5848
|
+
(record.retainedArtifactsSuccessorPath !== undefined &&
|
|
5849
|
+
!isRetainedNativePath(target, record.retainedArtifactsSuccessorPath)) ||
|
|
5850
|
+
(record.retainedArtifactsPlaceholderPath !== undefined &&
|
|
5851
|
+
!isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) ||
|
|
5852
|
+
(record.retainedArtifactsUnknownPath !== undefined &&
|
|
5853
|
+
!isRetainedNativePath(target, record.retainedArtifactsUnknownPath)) ||
|
|
5854
|
+
(record.retainedTranscriptSuccessorPath !== undefined &&
|
|
5855
|
+
!isRetainedNativePath(target, record.retainedTranscriptSuccessorPath)) ||
|
|
5856
|
+
(record.retainedTranscriptPlaceholderPath !== undefined &&
|
|
5857
|
+
!isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
|
|
5858
|
+
(record.retainedTranscriptUnknownPath !== undefined &&
|
|
5859
|
+
!isRetainedNativePath(target, record.retainedTranscriptUnknownPath)) ||
|
|
5860
|
+
(record.artifactsPayloadDurable !== undefined && record.artifactsPayloadDurable !== true) ||
|
|
5861
|
+
(record.artifactsRemovedAttempt !== undefined &&
|
|
5862
|
+
(typeof record.artifactsRemovedAttempt !== "number" ||
|
|
5863
|
+
!Number.isSafeInteger(record.artifactsRemovedAttempt) ||
|
|
5864
|
+
record.artifactsRemovedAttempt < 1 ||
|
|
5865
|
+
record.artifactsRemovedAttempt > (attempt as number))) ||
|
|
5866
|
+
(record.transcriptPayloadDurable !== undefined && record.transcriptPayloadDurable !== true) ||
|
|
5867
|
+
(record.taskArtifactOwnerTranscriptDeleted !== undefined &&
|
|
5868
|
+
(record.taskArtifactOwnerTranscriptDeleted !== true || !target.taskArtifactOwnerDeletionEvidence))
|
|
5869
|
+
)
|
|
5870
|
+
throw new Error("durability_failed");
|
|
5871
|
+
const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
|
|
5872
|
+
const expectedArtifactsIdentity = artifact
|
|
5873
|
+
? typeof artifact.dev === "string" &&
|
|
5874
|
+
typeof artifact.ino === "string" &&
|
|
5875
|
+
typeof artifact.size === "number" &&
|
|
5876
|
+
typeof artifact.mtimeNs === "string" &&
|
|
5877
|
+
typeof artifact.sha256 === "string"
|
|
5878
|
+
? {
|
|
5879
|
+
dev: BigInt(artifact.dev),
|
|
5880
|
+
ino: BigInt(artifact.ino),
|
|
5881
|
+
size: artifact.size,
|
|
5882
|
+
mtimeNs: BigInt(artifact.mtimeNs),
|
|
5883
|
+
sha256: artifact.sha256,
|
|
5884
|
+
}
|
|
5885
|
+
: undefined
|
|
5886
|
+
: undefined;
|
|
5887
|
+
if (artifact && !expectedArtifactsIdentity) throw new Error("durability_failed");
|
|
5888
|
+
const expectedArtifactsTree =
|
|
5889
|
+
record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
|
|
5890
|
+
if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
|
|
2968
5891
|
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
};
|
|
2999
|
-
}
|
|
3000
|
-
return latest;
|
|
3001
|
-
} catch (error) {
|
|
3002
|
-
if ((error as Error).message === "durability_failed") throw error;
|
|
3003
|
-
return undefined;
|
|
3004
|
-
}
|
|
5892
|
+
if (
|
|
5893
|
+
latest &&
|
|
5894
|
+
((record.detachedArtifactsPath !== undefined &&
|
|
5895
|
+
![...plannedPaths].some(planned => isAuthorizedArtifactRoot(target, planned, record.detachedArtifactsPath))) ||
|
|
5896
|
+
(record.detachedTranscriptPath !== undefined && !plannedPaths.has(record.detachedTranscriptPath)))
|
|
5897
|
+
)
|
|
5898
|
+
throw new Error("durability_failed");
|
|
5899
|
+
plannedPaths.add(record.plannedArtifactsPath as string);
|
|
5900
|
+
plannedPaths.add(record.plannedTranscriptPath as string);
|
|
5901
|
+
return {
|
|
5902
|
+
taskArtifactOwnerTranscriptDeleted: record.taskArtifactOwnerTranscriptDeleted === true ? true : undefined,
|
|
5903
|
+
attempt,
|
|
5904
|
+
target,
|
|
5905
|
+
expectedArtifactsIdentity,
|
|
5906
|
+
expectedArtifactsTree,
|
|
5907
|
+
artifactsPayloadDurable: record.artifactsPayloadDurable === true ? true : undefined,
|
|
5908
|
+
artifactsRemovedAttempt: record.artifactsRemovedAttempt as number | undefined,
|
|
5909
|
+
detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
|
|
5910
|
+
detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
|
|
5911
|
+
transcriptPayloadDurable: record.transcriptPayloadDurable === true ? true : undefined,
|
|
5912
|
+
retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
|
|
5913
|
+
retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
|
|
5914
|
+
retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
|
|
5915
|
+
retainedTranscriptSuccessorPath: record.retainedTranscriptSuccessorPath as string | undefined,
|
|
5916
|
+
retainedTranscriptPlaceholderPath: record.retainedTranscriptPlaceholderPath as string | undefined,
|
|
5917
|
+
retainedTranscriptUnknownPath: record.retainedTranscriptUnknownPath as string | undefined,
|
|
5918
|
+
plannedArtifactsPath: record.plannedArtifactsPath as string,
|
|
5919
|
+
plannedTranscriptPath: record.plannedTranscriptPath as string,
|
|
5920
|
+
};
|
|
3005
5921
|
}
|
|
3006
5922
|
|
|
3007
5923
|
function artifactIdentityForCleanup(target: RetiredTarget): SessionStorageFileIdentity | undefined {
|
|
@@ -3043,6 +5959,7 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
|
|
|
3043
5959
|
return {
|
|
3044
5960
|
attempt,
|
|
3045
5961
|
target,
|
|
5962
|
+
taskArtifactOwnerTranscriptDeleted: pending?.taskArtifactOwnerTranscriptDeleted,
|
|
3046
5963
|
expectedArtifactsIdentity,
|
|
3047
5964
|
expectedArtifactsTree,
|
|
3048
5965
|
artifactsPayloadDurable: pending?.artifactsPayloadDurable,
|
|
@@ -3076,6 +5993,8 @@ function cleanupPendingEvidence(
|
|
|
3076
5993
|
): CleanupReceipt {
|
|
3077
5994
|
return {
|
|
3078
5995
|
...retry,
|
|
5996
|
+
taskArtifactOwnerTranscriptDeleted:
|
|
5997
|
+
deletion.taskArtifactOwnerTranscriptDeleted ?? active.taskArtifactOwnerTranscriptDeleted,
|
|
3079
5998
|
expectedArtifactsIdentity:
|
|
3080
5999
|
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
3081
6000
|
expectedArtifactsTree: deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
@@ -3119,29 +6038,43 @@ async function continueDetachedArtifactCleanup(
|
|
|
3119
6038
|
lock: ManagedStorageLock,
|
|
3120
6039
|
flow: ManagedVerifiedDeleteTestEvent["flow"],
|
|
3121
6040
|
): Promise<{ deletion: VerifiedSessionDeleteResult; pendingEvidence: CleanupReceipt }> {
|
|
3122
|
-
const deletion = await deleteSessionVerifiedWithFence(
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
6041
|
+
const deletion = await deleteSessionVerifiedWithFence(
|
|
6042
|
+
flow,
|
|
6043
|
+
"artifact-finalization",
|
|
6044
|
+
lock,
|
|
6045
|
+
{
|
|
6046
|
+
sessionsRoot: scope.sessionsRoot,
|
|
6047
|
+
transcriptPath: target.path,
|
|
6048
|
+
sessionId: target.sessionId,
|
|
6049
|
+
cwd: target.cwd,
|
|
6050
|
+
transcriptIdentity: target.identity,
|
|
6051
|
+
transcriptParentIdentity: (() => {
|
|
6052
|
+
const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
6053
|
+
return { dev: parent.dev, ino: parent.ino };
|
|
6054
|
+
})(),
|
|
6055
|
+
expectedArtifactsIdentity: pendingEvidence.expectedArtifactsIdentity,
|
|
6056
|
+
expectedArtifactsTree: pendingEvidence.expectedArtifactsTree,
|
|
6057
|
+
detachedArtifactsPath: pendingEvidence.detachedArtifactsPath,
|
|
6058
|
+
retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
|
|
6059
|
+
retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
|
|
6060
|
+
retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
|
|
6061
|
+
detachedTranscriptPath: pendingEvidence.detachedTranscriptPath ?? fallbackDetachedTranscriptPath,
|
|
6062
|
+
retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
|
|
6063
|
+
retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
|
|
6064
|
+
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
6065
|
+
plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
|
|
6066
|
+
plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
|
|
6067
|
+
},
|
|
6068
|
+
scope,
|
|
6069
|
+
target,
|
|
6070
|
+
);
|
|
6071
|
+
if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
|
|
6072
|
+
await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
6073
|
+
const retry = nextCleanupReceipt(target, pendingEvidence);
|
|
6074
|
+
const pending = cleanupPendingEvidence(retry, pendingEvidence, deletion);
|
|
6075
|
+
await publishCleanupPending(scope, tombstone, pending, lock);
|
|
6076
|
+
return { deletion, pendingEvidence: pending };
|
|
6077
|
+
}
|
|
3145
6078
|
if (deletion.kind === "cleanup_pending") {
|
|
3146
6079
|
if (
|
|
3147
6080
|
deletion.phase !== "artifacts" ||
|
|
@@ -3180,33 +6113,110 @@ async function publishCleanupPending(
|
|
|
3180
6113
|
if (!persisted || persisted.attempt !== receipt.attempt) throw new Error("durability_failed");
|
|
3181
6114
|
}
|
|
3182
6115
|
|
|
3183
|
-
function
|
|
6116
|
+
async function persistManagedOwnerTranscriptDeletion(
|
|
6117
|
+
scope: ManagedScope,
|
|
6118
|
+
tombstone: string,
|
|
6119
|
+
target: RetiredTarget,
|
|
6120
|
+
fallback: CleanupReceipt,
|
|
6121
|
+
lock: ManagedStorageLock,
|
|
6122
|
+
deletion: Extract<VerifiedSessionDeleteResult, { kind: "deleted" }>,
|
|
6123
|
+
): Promise<void> {
|
|
6124
|
+
return persistManagedGcOwnerTranscriptDeletion({
|
|
6125
|
+
target,
|
|
6126
|
+
fallback,
|
|
6127
|
+
deletion,
|
|
6128
|
+
readLatest: () => pendingCleanupReceipt(scope, tombstone, target),
|
|
6129
|
+
nextReceipt: latest => nextCleanupReceipt(target, latest),
|
|
6130
|
+
publishPending: receipt => publishCleanupPending(scope, tombstone, receipt, lock),
|
|
6131
|
+
});
|
|
6132
|
+
}
|
|
6133
|
+
|
|
6134
|
+
async function cleanupCompleted(scope: ManagedScope, tombstone: string, target: RetiredTarget): Promise<boolean> {
|
|
3184
6135
|
try {
|
|
6136
|
+
const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
|
|
6137
|
+
? await readManagedGcSessionRetirementReceipt(scope, target.path)
|
|
6138
|
+
: undefined;
|
|
6139
|
+
if (
|
|
6140
|
+
target.taskArtifactOwnerDeletionEvidence &&
|
|
6141
|
+
(ownerReceipt?.state !== "owner_retired" ||
|
|
6142
|
+
!deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
|
|
6143
|
+
)
|
|
6144
|
+
return false;
|
|
3185
6145
|
const value: unknown = JSON.parse(
|
|
3186
6146
|
captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "completed", 1)).bytes.toString("utf8"),
|
|
3187
6147
|
);
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
} catch {
|
|
6148
|
+
return cleanupCompletedRecordMatches(scope, tombstone, target, ownerReceipt, value);
|
|
6149
|
+
} catch (error) {
|
|
6150
|
+
if (isOwnerBoundaryFailure(error) || isManagedGcJournalCapacityError(error)) throw error;
|
|
6151
|
+
return false;
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
function cleanupCompletedRecordMatches(
|
|
6156
|
+
scope: ManagedScope,
|
|
6157
|
+
tombstone: string,
|
|
6158
|
+
target: RetiredTarget,
|
|
6159
|
+
ownerReceipt: ManagedGcSessionRetirementReceipt | undefined,
|
|
6160
|
+
value: unknown,
|
|
6161
|
+
): boolean {
|
|
6162
|
+
if (
|
|
6163
|
+
target.taskArtifactOwnerDeletionEvidence &&
|
|
6164
|
+
(ownerReceipt?.state !== "owner_retired" ||
|
|
6165
|
+
!deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
|
|
6166
|
+
)
|
|
3208
6167
|
return false;
|
|
6168
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
6169
|
+
const record = value as Record<string, unknown>;
|
|
6170
|
+
const recorded = record.target as Record<string, unknown> | undefined;
|
|
6171
|
+
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
6172
|
+
const expectedKeys = ownerReceipt
|
|
6173
|
+
? [
|
|
6174
|
+
"schemaVersion",
|
|
6175
|
+
"state",
|
|
6176
|
+
"scope",
|
|
6177
|
+
"tombstone",
|
|
6178
|
+
"attempt",
|
|
6179
|
+
"target",
|
|
6180
|
+
"taskArtifactOwnerDeletionEvidence",
|
|
6181
|
+
"taskArtifactOwnerRetirementOutcome",
|
|
6182
|
+
"taskArtifactOwnerRetired",
|
|
6183
|
+
"taskArtifactOwnerTranscriptDeleted",
|
|
6184
|
+
]
|
|
6185
|
+
: ["schemaVersion", "state", "scope", "tombstone", "attempt", "target"];
|
|
6186
|
+
if (Object.keys(record).length !== expectedKeys.length || !expectedKeys.every(key => key in record)) return false;
|
|
6187
|
+
if (ownerReceipt) {
|
|
6188
|
+
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
6189
|
+
if (record.taskArtifactOwnerTranscriptDeleted !== true || !pending?.taskArtifactOwnerTranscriptDeleted)
|
|
6190
|
+
return false;
|
|
6191
|
+
const evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
|
|
6192
|
+
const outcome = parseTaskArtifactOwnerRetirementOutcome(
|
|
6193
|
+
taskArtifactOwnerStorageContextForScope(scope),
|
|
6194
|
+
evidence,
|
|
6195
|
+
record.taskArtifactOwnerRetirementOutcome,
|
|
6196
|
+
);
|
|
6197
|
+
if (
|
|
6198
|
+
record.taskArtifactOwnerRetired !== true ||
|
|
6199
|
+
outcome.kind !== "completed" ||
|
|
6200
|
+
!deepSame(evidence, ownerReceipt.taskArtifactOwnerDeletionEvidence) ||
|
|
6201
|
+
!deepSame(outcome, ownerReceipt.taskArtifactOwnerRetirementOutcome)
|
|
6202
|
+
)
|
|
6203
|
+
return false;
|
|
3209
6204
|
}
|
|
6205
|
+
return (
|
|
6206
|
+
record.schemaVersion === 1 &&
|
|
6207
|
+
record.state === "cleanup_completed" &&
|
|
6208
|
+
record.scope === scopeDigest(scope.platform, scope.canonicalCwd) &&
|
|
6209
|
+
record.tombstone === tombstone &&
|
|
6210
|
+
record.attempt === 1 &&
|
|
6211
|
+
recorded?.path === target.path &&
|
|
6212
|
+
recorded.sessionId === target.sessionId &&
|
|
6213
|
+
recorded.cwd === target.cwd &&
|
|
6214
|
+
identity?.dev === String(target.identity.dev) &&
|
|
6215
|
+
identity.ino === String(target.identity.ino) &&
|
|
6216
|
+
identity.size === target.identity.size &&
|
|
6217
|
+
identity.mtimeNs === String(target.identity.mtimeNs) &&
|
|
6218
|
+
identity.sha256 === target.identity.sha256
|
|
6219
|
+
);
|
|
3210
6220
|
}
|
|
3211
6221
|
|
|
3212
6222
|
async function publishCleanupCompleted(
|
|
@@ -3215,6 +6225,22 @@ async function publishCleanupCompleted(
|
|
|
3215
6225
|
target: RetiredTarget,
|
|
3216
6226
|
lock: ManagedStorageLock,
|
|
3217
6227
|
): Promise<void> {
|
|
6228
|
+
const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
|
|
6229
|
+
? await readManagedGcSessionRetirementReceipt(scope, target.path)
|
|
6230
|
+
: undefined;
|
|
6231
|
+
if (
|
|
6232
|
+
target.taskArtifactOwnerDeletionEvidence &&
|
|
6233
|
+
(ownerReceipt?.state !== "owner_retired" ||
|
|
6234
|
+
!deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
|
|
6235
|
+
)
|
|
6236
|
+
throw new Error("durability_failed");
|
|
6237
|
+
const ownerOutcome = ownerReceipt?.taskArtifactOwnerRetirementOutcome;
|
|
6238
|
+
if (
|
|
6239
|
+
ownerReceipt &&
|
|
6240
|
+
(ownerOutcome?.kind !== "completed" ||
|
|
6241
|
+
!pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted)
|
|
6242
|
+
)
|
|
6243
|
+
throw new Error("durability_failed");
|
|
3218
6244
|
try {
|
|
3219
6245
|
await publishManagedTombstone(
|
|
3220
6246
|
cleanupReceiptPath(tombstone, target, "completed", 1),
|
|
@@ -3225,12 +6251,21 @@ async function publishCleanupCompleted(
|
|
|
3225
6251
|
tombstone,
|
|
3226
6252
|
attempt: 1,
|
|
3227
6253
|
target: { path: target.path, sessionId: target.sessionId, cwd: target.cwd, identity: target.identity },
|
|
6254
|
+
...(ownerReceipt && ownerOutcome
|
|
6255
|
+
? {
|
|
6256
|
+
taskArtifactOwnerDeletionEvidence: ownerReceipt.taskArtifactOwnerDeletionEvidence,
|
|
6257
|
+
taskArtifactOwnerRetirementOutcome: ownerOutcome,
|
|
6258
|
+
taskArtifactOwnerRetired: true,
|
|
6259
|
+
taskArtifactOwnerTranscriptDeleted: true,
|
|
6260
|
+
}
|
|
6261
|
+
: {}),
|
|
3228
6262
|
},
|
|
3229
6263
|
lock.assertOwned,
|
|
3230
6264
|
);
|
|
3231
6265
|
} catch (error) {
|
|
3232
6266
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
3233
6267
|
}
|
|
6268
|
+
if (!(await cleanupCompleted(scope, tombstone, target))) throw new Error("durability_failed");
|
|
3234
6269
|
}
|
|
3235
6270
|
|
|
3236
6271
|
function tombstonePathContaining(scope: ManagedScope, candidate: ManagedCandidate): string | undefined {
|
|
@@ -3240,7 +6275,8 @@ function tombstonePathContaining(scope: ManagedScope, candidate: ManagedCandidat
|
|
|
3240
6275
|
const pathname = path.join(directory, name);
|
|
3241
6276
|
if (retiredTargets(scope, pathname)?.some(target => sameCandidate(target, candidate))) return pathname;
|
|
3242
6277
|
}
|
|
3243
|
-
} catch {
|
|
6278
|
+
} catch (error) {
|
|
6279
|
+
if (isOwnerBoundaryFailure(error) || (error as Error).message === "durability_failed") throw error;
|
|
3244
6280
|
return undefined;
|
|
3245
6281
|
}
|
|
3246
6282
|
return undefined;
|
|
@@ -3554,7 +6590,7 @@ export function cleanupAuthorityMatches(
|
|
|
3554
6590
|
parentStat.ino !== cleanup.identity.parentIno
|
|
3555
6591
|
)
|
|
3556
6592
|
return false;
|
|
3557
|
-
const snapshot = nativeScope().
|
|
6593
|
+
const snapshot = nativeScope().snapshotEmptyDirectory(cleanup.retainedPath);
|
|
3558
6594
|
const observedRoot = snapshot.snapshot?.entries.find(
|
|
3559
6595
|
entry => entry.relativePath === "" && entry.kind === "directory",
|
|
3560
6596
|
);
|
|
@@ -3618,7 +6654,7 @@ export function detachArtifactRootForMigration(
|
|
|
3618
6654
|
const stat = fs.lstatSync(placeholder, { bigint: true });
|
|
3619
6655
|
if (!stat.isDirectory() || stat.isSymbolicLink() || path.dirname(placeholder) !== path.dirname(plan.originalPath))
|
|
3620
6656
|
throw new Error("durability_failed");
|
|
3621
|
-
const snapshot = nativeScope().
|
|
6657
|
+
const snapshot = nativeScope().snapshotEmptyDirectory(placeholder);
|
|
3622
6658
|
if (!snapshot.ok || !snapshot.snapshot) throw new Error("durability_failed");
|
|
3623
6659
|
// Windows directory size/mtime authority is the native tree root, never Bun's
|
|
3624
6660
|
// zero-valued directory lstat. Capturing Bun values here would guarantee a
|
|
@@ -3983,11 +7019,23 @@ export async function reconcileManagedTombstones(
|
|
|
3983
7019
|
expectedCandidate?: ManagedCandidate,
|
|
3984
7020
|
): Promise<void> {
|
|
3985
7021
|
const directory = path.join(managedInternalDirectory(scope), MANAGED_TOMBSTONES_DIRECTORY);
|
|
7022
|
+
// Ensure scope authority is prepared before any operations that might access the GC protocol
|
|
7023
|
+
const prepared = await ensureManagedScope(
|
|
7024
|
+
scope,
|
|
7025
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
7026
|
+
);
|
|
7027
|
+
if (prepared.kind === "error") throw new Error(prepared.message);
|
|
3986
7028
|
for (const name of fs.readdirSync(directory)) {
|
|
3987
7029
|
const tombstone = path.join(directory, name);
|
|
3988
|
-
const
|
|
3989
|
-
if (!
|
|
3990
|
-
|
|
7030
|
+
const discoveredTargets = retiredTargets(scope, tombstone);
|
|
7031
|
+
if (!discoveredTargets || discoveredTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target)))
|
|
7032
|
+
continue;
|
|
7033
|
+
if (
|
|
7034
|
+
await Promise.all(discoveredTargets.map(target => cleanupCompleted(scope, tombstone, target))).then(values =>
|
|
7035
|
+
values.every(Boolean),
|
|
7036
|
+
)
|
|
7037
|
+
)
|
|
7038
|
+
continue;
|
|
3991
7039
|
let lock: ManagedStorageLock | undefined;
|
|
3992
7040
|
try {
|
|
3993
7041
|
lock = await acquireManagedLock(
|
|
@@ -3995,11 +7043,22 @@ export async function reconcileManagedTombstones(
|
|
|
3995
7043
|
path.basename(tombstone, ".json"),
|
|
3996
7044
|
scopeRoot(scope),
|
|
3997
7045
|
);
|
|
3998
|
-
const
|
|
3999
|
-
if (
|
|
7046
|
+
const discoveredLockedTargets = retiredTargets(scope, tombstone);
|
|
7047
|
+
if (
|
|
7048
|
+
!discoveredLockedTargets ||
|
|
7049
|
+
discoveredLockedTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target))
|
|
7050
|
+
)
|
|
7051
|
+
continue;
|
|
7052
|
+
const lockedTargets = await prepareManagedGcOwnerTargets(scope, discoveredLockedTargets, lock);
|
|
7053
|
+
if (
|
|
7054
|
+
await Promise.all(lockedTargets.map(target => cleanupCompleted(scope, tombstone, target))).then(values =>
|
|
7055
|
+
values.every(Boolean),
|
|
7056
|
+
)
|
|
7057
|
+
)
|
|
7058
|
+
continue;
|
|
4000
7059
|
for (const target of lockedTargets) {
|
|
4001
7060
|
lock.assertOwned();
|
|
4002
|
-
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
7061
|
+
if (await cleanupCompleted(scope, tombstone, target)) continue;
|
|
4003
7062
|
try {
|
|
4004
7063
|
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
4005
7064
|
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
@@ -4024,8 +7083,23 @@ export async function reconcileManagedTombstones(
|
|
|
4024
7083
|
target,
|
|
4025
7084
|
replayReceipt,
|
|
4026
7085
|
artifactsEvidence.retainedArtifactsRootPath,
|
|
4027
|
-
)
|
|
7086
|
+
) &&
|
|
7087
|
+
(!target.taskArtifactOwnerDeletionEvidence ||
|
|
7088
|
+
pending?.taskArtifactOwnerTranscriptDeleted === true)
|
|
4028
7089
|
) {
|
|
7090
|
+
// A conflicting legacy completion record is not fresh completion authority.
|
|
7091
|
+
if (
|
|
7092
|
+
!target.taskArtifactOwnerDeletionEvidence &&
|
|
7093
|
+
fs.existsSync(cleanupReceiptPath(tombstone, target, "completed", 1))
|
|
7094
|
+
)
|
|
7095
|
+
continue;
|
|
7096
|
+
const ownerProgress = await retireManagedGcOwnerAfterArtifacts(
|
|
7097
|
+
scope,
|
|
7098
|
+
target,
|
|
7099
|
+
lockedTargets,
|
|
7100
|
+
lock,
|
|
7101
|
+
);
|
|
7102
|
+
if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired") continue;
|
|
4029
7103
|
fsyncManagedParent(target.path);
|
|
4030
7104
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4031
7105
|
continue;
|
|
@@ -4050,38 +7124,11 @@ export async function reconcileManagedTombstones(
|
|
|
4050
7124
|
? target
|
|
4051
7125
|
: validateCandidateForScope(scope, target);
|
|
4052
7126
|
if (!initialTarget) throw new Error("source_changed");
|
|
4053
|
-
let
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
transcriptIdentity: {
|
|
4059
|
-
...initialTarget.identity,
|
|
4060
|
-
nlink: fs.lstatSync(observedPending?.detachedTranscriptPath ?? target.path, { bigint: true })
|
|
4061
|
-
.nlink,
|
|
4062
|
-
},
|
|
4063
|
-
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
4064
|
-
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
4065
|
-
detachedArtifactsPath:
|
|
4066
|
-
active.detachedArtifactsPath ??
|
|
4067
|
-
observedPending?.detachedArtifactsPath ??
|
|
4068
|
-
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
4069
|
-
detachedTranscriptPath:
|
|
4070
|
-
active.detachedTranscriptPath ??
|
|
4071
|
-
observedPending?.detachedTranscriptPath ??
|
|
4072
|
-
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
4073
|
-
? pending.plannedTranscriptPath
|
|
4074
|
-
: undefined) ??
|
|
4075
|
-
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
4076
|
-
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
4077
|
-
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
4078
|
-
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
4079
|
-
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
4080
|
-
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
4081
|
-
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
4082
|
-
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
4083
|
-
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
4084
|
-
...(cleanupArtifactsRemoved(
|
|
7127
|
+
let ownerProgress = await managedGcOwnerProgressBeforeDelete(
|
|
7128
|
+
scope,
|
|
7129
|
+
target,
|
|
7130
|
+
lockedTargets,
|
|
7131
|
+
cleanupArtifactsRemoved(
|
|
4085
7132
|
scope,
|
|
4086
7133
|
tombstone,
|
|
4087
7134
|
target,
|
|
@@ -4089,12 +7136,65 @@ export async function reconcileManagedTombstones(
|
|
|
4089
7136
|
active.artifactsRemovedAttempt ??
|
|
4090
7137
|
pending?.attempt ??
|
|
4091
7138
|
active.attempt,
|
|
4092
|
-
)
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
7139
|
+
),
|
|
7140
|
+
lock,
|
|
7141
|
+
);
|
|
7142
|
+
if (ownerProgress.state === "pending") continue;
|
|
7143
|
+
let deletion = await deleteSessionVerifiedWithFence(
|
|
7144
|
+
"reconcile",
|
|
7145
|
+
"initial",
|
|
7146
|
+
lock,
|
|
7147
|
+
{
|
|
7148
|
+
sessionsRoot: scope.sessionsRoot,
|
|
7149
|
+
transcriptPath: target.path,
|
|
7150
|
+
sessionId: target.sessionId,
|
|
7151
|
+
cwd: target.cwd,
|
|
7152
|
+
transcriptIdentity: {
|
|
7153
|
+
...initialTarget.identity,
|
|
7154
|
+
nlink: fs.lstatSync(observedPending?.detachedTranscriptPath ?? target.path, { bigint: true })
|
|
7155
|
+
.nlink,
|
|
7156
|
+
},
|
|
7157
|
+
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
7158
|
+
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
7159
|
+
detachedArtifactsPath:
|
|
7160
|
+
active.detachedArtifactsPath ??
|
|
7161
|
+
observedPending?.detachedArtifactsPath ??
|
|
7162
|
+
(fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
|
|
7163
|
+
detachedTranscriptPath:
|
|
7164
|
+
active.detachedTranscriptPath ??
|
|
7165
|
+
observedPending?.detachedTranscriptPath ??
|
|
7166
|
+
(pending && fs.existsSync(pending.plannedTranscriptPath)
|
|
7167
|
+
? pending.plannedTranscriptPath
|
|
7168
|
+
: undefined) ??
|
|
7169
|
+
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
7170
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
7171
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
7172
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
7173
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
7174
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
7175
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
7176
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
7177
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
7178
|
+
...(cleanupArtifactsRemoved(
|
|
7179
|
+
scope,
|
|
7180
|
+
tombstone,
|
|
7181
|
+
target,
|
|
7182
|
+
pending?.artifactsRemovedAttempt ??
|
|
7183
|
+
active.artifactsRemovedAttempt ??
|
|
7184
|
+
pending?.attempt ??
|
|
7185
|
+
active.attempt,
|
|
7186
|
+
)
|
|
7187
|
+
? { artifactsRemoved: true as const }
|
|
7188
|
+
: {}),
|
|
7189
|
+
},
|
|
7190
|
+
scope,
|
|
7191
|
+
target,
|
|
7192
|
+
);
|
|
4096
7193
|
if (deletion.kind === "artifacts_removed") {
|
|
7194
|
+
await publishManagedGcArtifactsRemoved(scope, target, lock);
|
|
4097
7195
|
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
7196
|
+
ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, lockedTargets, lock);
|
|
7197
|
+
if (ownerProgress.state === "pending") continue;
|
|
4098
7198
|
active = { ...active, artifactsRemovedAttempt: active.attempt };
|
|
4099
7199
|
const refreshedTarget = validateCandidateForScope(scope, target);
|
|
4100
7200
|
if (!refreshedTarget) throw new Error("source_changed");
|
|
@@ -4121,9 +7221,15 @@ export async function reconcileManagedTombstones(
|
|
|
4121
7221
|
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
4122
7222
|
artifactsRemoved: true,
|
|
4123
7223
|
},
|
|
7224
|
+
scope,
|
|
7225
|
+
target,
|
|
4124
7226
|
);
|
|
4125
7227
|
}
|
|
4126
7228
|
if (deletion.kind === "cleanup_pending") {
|
|
7229
|
+
if (deletion.phase === "task_artifact_owner") {
|
|
7230
|
+
ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
7231
|
+
continue;
|
|
7232
|
+
}
|
|
4127
7233
|
assertAuthorizedCleanupPending(target, active, deletion);
|
|
4128
7234
|
const retry = nextCleanupReceipt(target, active);
|
|
4129
7235
|
let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
|
|
@@ -4150,7 +7256,10 @@ export async function reconcileManagedTombstones(
|
|
|
4150
7256
|
!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
|
|
4151
7257
|
)
|
|
4152
7258
|
continue;
|
|
7259
|
+
await publishManagedGcArtifactsRemoved(scope, target, lock);
|
|
4153
7260
|
await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
|
|
7261
|
+
ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, lockedTargets, lock);
|
|
7262
|
+
if (ownerProgress.state === "pending") continue;
|
|
4154
7263
|
pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
|
|
4155
7264
|
const retainedProof = cleanupArtifactsRemovedReceipt(
|
|
4156
7265
|
tombstone,
|
|
@@ -4186,6 +7295,8 @@ export async function reconcileManagedTombstones(
|
|
|
4186
7295
|
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
4187
7296
|
artifactsRemoved: true,
|
|
4188
7297
|
},
|
|
7298
|
+
scope,
|
|
7299
|
+
target,
|
|
4189
7300
|
() => {
|
|
4190
7301
|
if (
|
|
4191
7302
|
!cleanupArtifactsRemoved(
|
|
@@ -4198,6 +7309,10 @@ export async function reconcileManagedTombstones(
|
|
|
4198
7309
|
throw new Error("durability_failed");
|
|
4199
7310
|
},
|
|
4200
7311
|
);
|
|
7312
|
+
if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
|
|
7313
|
+
ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
7314
|
+
continue;
|
|
7315
|
+
}
|
|
4201
7316
|
if (deletion.kind === "cleanup_pending") {
|
|
4202
7317
|
if (
|
|
4203
7318
|
deletion.phase !== "transcript" ||
|
|
@@ -4231,9 +7346,19 @@ export async function reconcileManagedTombstones(
|
|
|
4231
7346
|
)
|
|
4232
7347
|
continue;
|
|
4233
7348
|
}
|
|
7349
|
+
if (deletion.kind === "deleted")
|
|
7350
|
+
await persistManagedOwnerTranscriptDeletion(scope, tombstone, target, active, lock, deletion);
|
|
7351
|
+
const ownerDisposition = await taskArtifactOwnerTranscriptResult(scope, target, lock);
|
|
7352
|
+
if (
|
|
7353
|
+
(ownerDisposition !== "none" && ownerDisposition !== "retired") ||
|
|
7354
|
+
(target.taskArtifactOwnerDeletionEvidence &&
|
|
7355
|
+
!pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted)
|
|
7356
|
+
)
|
|
7357
|
+
continue;
|
|
4234
7358
|
fsyncManagedParent(target.path);
|
|
4235
7359
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4236
7360
|
} catch (error) {
|
|
7361
|
+
if (isManagedGcJournalCapacityError(error)) throw error;
|
|
4237
7362
|
if (!expectedCandidate || target.sessionId === expectedCandidate.sessionId) throw error;
|
|
4238
7363
|
logger.warn("Tombstone reconciliation failed for one target; will retry on a future scope open", {
|
|
4239
7364
|
tombstone,
|
|
@@ -4651,6 +7776,24 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4651
7776
|
scope: ManagedScope,
|
|
4652
7777
|
candidate: ManagedCandidate,
|
|
4653
7778
|
): Promise<ManagedDeleteCandidateResult> {
|
|
7779
|
+
try {
|
|
7780
|
+
if (hasUnsupportedLegacyOwnerTargetInternal(scope.directoryPath, candidate)) {
|
|
7781
|
+
const tombstonePath = path.join(
|
|
7782
|
+
managedInternalDirectory(scope),
|
|
7783
|
+
MANAGED_TOMBSTONES_DIRECTORY,
|
|
7784
|
+
`${stableOperationName(candidate)}.json`,
|
|
7785
|
+
);
|
|
7786
|
+
return {
|
|
7787
|
+
kind: "cleanup_pending",
|
|
7788
|
+
tombstonePath,
|
|
7789
|
+
phase: "artifacts",
|
|
7790
|
+
message: "task_artifact_owner_legacy_scope_unsupported",
|
|
7791
|
+
};
|
|
7792
|
+
}
|
|
7793
|
+
} catch (error) {
|
|
7794
|
+
const code = expectedFailure(error);
|
|
7795
|
+
return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
|
|
7796
|
+
}
|
|
4654
7797
|
const prepared = await prepareManagedSessionScopeForWrite(scope);
|
|
4655
7798
|
if (prepared.kind === "error")
|
|
4656
7799
|
return {
|
|
@@ -4660,6 +7803,12 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4660
7803
|
};
|
|
4661
7804
|
const current = validateCandidateForScope(scope, candidate);
|
|
4662
7805
|
const paired = current ? receiptPair(scope, current) : undefined;
|
|
7806
|
+
if (paired)
|
|
7807
|
+
assertOwnerTranscriptLocator({
|
|
7808
|
+
transcriptPath: paired.path,
|
|
7809
|
+
sessionId: paired.sessionId,
|
|
7810
|
+
transcriptIdentity: paired.identity,
|
|
7811
|
+
});
|
|
4663
7812
|
const logical = paired?.provenance === "legacy" ? paired : (current ?? candidate);
|
|
4664
7813
|
const existingTombstone = tombstonePathContaining(scope, candidate);
|
|
4665
7814
|
const tombstone =
|
|
@@ -4676,9 +7825,17 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4676
7825
|
scopeRoot(scope),
|
|
4677
7826
|
);
|
|
4678
7827
|
let targets = retiredTargets(scope, tombstone);
|
|
7828
|
+
const initialTargets = targets ?? (current ? [current, ...(paired ? [paired] : [])] : []);
|
|
7829
|
+
if (initialTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target)))
|
|
7830
|
+
return {
|
|
7831
|
+
kind: "cleanup_pending",
|
|
7832
|
+
tombstonePath: tombstone,
|
|
7833
|
+
phase: "artifacts",
|
|
7834
|
+
message: "task_artifact_owner_legacy_scope_unsupported",
|
|
7835
|
+
};
|
|
4679
7836
|
if (!targets) {
|
|
4680
7837
|
if (!current) throw new Error("source_changed");
|
|
4681
|
-
targets = [current, ...(paired ? [paired] : [])];
|
|
7838
|
+
targets = await prepareManagedGcOwnerTargets(scope, [current, ...(paired ? [paired] : [])], lock);
|
|
4682
7839
|
lock.assertOwned();
|
|
4683
7840
|
try {
|
|
4684
7841
|
await publishManagedTombstone(
|
|
@@ -4693,6 +7850,9 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4693
7850
|
cwd: target.cwd,
|
|
4694
7851
|
provenance: target.provenance,
|
|
4695
7852
|
identity: target.identity,
|
|
7853
|
+
...(target.taskArtifactOwnerDeletionEvidence
|
|
7854
|
+
? { taskArtifactOwnerDeletionEvidence: target.taskArtifactOwnerDeletionEvidence }
|
|
7855
|
+
: {}),
|
|
4696
7856
|
})),
|
|
4697
7857
|
},
|
|
4698
7858
|
lock.assertOwned,
|
|
@@ -4703,6 +7863,7 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4703
7863
|
targets = retiredTargets(scope, tombstone);
|
|
4704
7864
|
if (!targets) throw new Error("durability_failed");
|
|
4705
7865
|
}
|
|
7866
|
+
targets = await prepareManagedGcOwnerTargets(scope, targets, lock);
|
|
4706
7867
|
let deletedAny = false;
|
|
4707
7868
|
for (const target of targets) {
|
|
4708
7869
|
lock.assertOwned();
|
|
@@ -4712,6 +7873,42 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4712
7873
|
fs.lstatSync(target.path);
|
|
4713
7874
|
} catch (error) {
|
|
4714
7875
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
7876
|
+
if (!observedPending && target.taskArtifactOwnerDeletionEvidence) {
|
|
7877
|
+
if (!pending?.taskArtifactOwnerTranscriptDeleted)
|
|
7878
|
+
return {
|
|
7879
|
+
kind: "cleanup_pending",
|
|
7880
|
+
tombstonePath: tombstone,
|
|
7881
|
+
phase: "artifacts",
|
|
7882
|
+
message: "task_artifact_owner_transcript_deletion_unverified",
|
|
7883
|
+
};
|
|
7884
|
+
const artifactsEvidence = cleanupArtifactsRemovedEvidence(
|
|
7885
|
+
scope,
|
|
7886
|
+
tombstone,
|
|
7887
|
+
target,
|
|
7888
|
+
pending.artifactsRemovedAttempt ?? pending.attempt,
|
|
7889
|
+
);
|
|
7890
|
+
if (
|
|
7891
|
+
!artifactsEvidence ||
|
|
7892
|
+
!cleanupRootsAbsent(tombstone, target, pending, artifactsEvidence.retainedArtifactsRootPath)
|
|
7893
|
+
)
|
|
7894
|
+
return {
|
|
7895
|
+
kind: "cleanup_pending",
|
|
7896
|
+
tombstonePath: tombstone,
|
|
7897
|
+
phase: "artifacts",
|
|
7898
|
+
message: "task_artifact_owner_retirement_pending",
|
|
7899
|
+
};
|
|
7900
|
+
const ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
|
|
7901
|
+
if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired")
|
|
7902
|
+
return {
|
|
7903
|
+
kind: "cleanup_pending",
|
|
7904
|
+
tombstonePath: tombstone,
|
|
7905
|
+
phase: "artifacts",
|
|
7906
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
7907
|
+
};
|
|
7908
|
+
fsyncManagedParent(target.path);
|
|
7909
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
7910
|
+
continue;
|
|
7911
|
+
}
|
|
4715
7912
|
if (!observedPending) continue;
|
|
4716
7913
|
const artifactsEvidence = cleanupArtifactsRemovedEvidence(
|
|
4717
7914
|
scope,
|
|
@@ -4721,15 +7918,31 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4721
7918
|
);
|
|
4722
7919
|
if (
|
|
4723
7920
|
artifactsEvidence &&
|
|
4724
|
-
cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath)
|
|
7921
|
+
cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath) &&
|
|
7922
|
+
(!target.taskArtifactOwnerDeletionEvidence || pending?.taskArtifactOwnerTranscriptDeleted === true)
|
|
4725
7923
|
) {
|
|
7924
|
+
const ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
|
|
7925
|
+
if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired")
|
|
7926
|
+
return {
|
|
7927
|
+
kind: "cleanup_pending",
|
|
7928
|
+
tombstonePath: tombstone,
|
|
7929
|
+
phase: "artifacts",
|
|
7930
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
7931
|
+
};
|
|
4726
7932
|
fsyncManagedParent(target.path);
|
|
4727
7933
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4728
7934
|
continue;
|
|
4729
7935
|
}
|
|
7936
|
+
if (target.taskArtifactOwnerDeletionEvidence && !observedPending.detachedTranscriptPath)
|
|
7937
|
+
return {
|
|
7938
|
+
kind: "cleanup_pending",
|
|
7939
|
+
tombstonePath: tombstone,
|
|
7940
|
+
phase: "artifacts",
|
|
7941
|
+
message: "task_artifact_owner_transcript_deletion_unverified",
|
|
7942
|
+
};
|
|
4730
7943
|
} else throw error;
|
|
4731
7944
|
}
|
|
4732
|
-
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
7945
|
+
if (await cleanupCompleted(scope, tombstone, target)) continue;
|
|
4733
7946
|
deletedAny = true;
|
|
4734
7947
|
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
4735
7948
|
if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
|
|
@@ -4745,64 +7958,118 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4745
7958
|
await publishCleanupPending(scope, tombstone, active, lock);
|
|
4746
7959
|
const initialTarget = validateCandidateForScope(scope, target);
|
|
4747
7960
|
if (!initialTarget) throw new Error("source_changed");
|
|
4748
|
-
let
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
transcriptIdentity: initialTarget.identity,
|
|
4754
|
-
transcriptParentIdentity: (() => {
|
|
4755
|
-
const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
4756
|
-
return { dev: parent.dev, ino: parent.ino };
|
|
4757
|
-
})(),
|
|
4758
|
-
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
4759
|
-
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
4760
|
-
detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
|
|
4761
|
-
detachedTranscriptPath:
|
|
4762
|
-
active.detachedTranscriptPath ??
|
|
4763
|
-
observedPending?.detachedTranscriptPath ??
|
|
4764
|
-
(pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
|
|
4765
|
-
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
4766
|
-
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
4767
|
-
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
4768
|
-
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
4769
|
-
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
4770
|
-
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
4771
|
-
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
4772
|
-
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
4773
|
-
...(cleanupArtifactsRemoved(
|
|
7961
|
+
let ownerProgress = await managedGcOwnerProgressBeforeDelete(
|
|
7962
|
+
scope,
|
|
7963
|
+
target,
|
|
7964
|
+
targets,
|
|
7965
|
+
cleanupArtifactsRemoved(
|
|
4774
7966
|
scope,
|
|
4775
7967
|
tombstone,
|
|
4776
7968
|
target,
|
|
4777
7969
|
pending?.artifactsRemovedAttempt ?? active.artifactsRemovedAttempt ?? pending?.attempt ?? active.attempt,
|
|
4778
|
-
)
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
7970
|
+
),
|
|
7971
|
+
lock,
|
|
7972
|
+
);
|
|
7973
|
+
if (ownerProgress.state === "pending")
|
|
7974
|
+
return {
|
|
7975
|
+
kind: "cleanup_pending",
|
|
7976
|
+
tombstonePath: tombstone,
|
|
7977
|
+
phase: "artifacts",
|
|
7978
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
7979
|
+
};
|
|
7980
|
+
let deletion = await deleteSessionVerifiedWithFence(
|
|
7981
|
+
"direct",
|
|
7982
|
+
"initial",
|
|
7983
|
+
lock,
|
|
7984
|
+
{
|
|
4788
7985
|
sessionsRoot: scope.sessionsRoot,
|
|
4789
7986
|
transcriptPath: target.path,
|
|
4790
7987
|
sessionId: target.sessionId,
|
|
4791
7988
|
cwd: target.cwd,
|
|
4792
|
-
transcriptIdentity:
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
7989
|
+
transcriptIdentity: initialTarget.identity,
|
|
7990
|
+
transcriptParentIdentity: (() => {
|
|
7991
|
+
const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
7992
|
+
return { dev: parent.dev, ino: parent.ino };
|
|
7993
|
+
})(),
|
|
7994
|
+
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
7995
|
+
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
7996
|
+
detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
|
|
7997
|
+
detachedTranscriptPath:
|
|
7998
|
+
active.detachedTranscriptPath ??
|
|
7999
|
+
observedPending?.detachedTranscriptPath ??
|
|
8000
|
+
(pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
|
|
4796
8001
|
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
4797
8002
|
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
4798
8003
|
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
4799
8004
|
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
4800
8005
|
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
4801
8006
|
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
4802
|
-
|
|
4803
|
-
|
|
8007
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
8008
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
8009
|
+
...(cleanupArtifactsRemoved(
|
|
8010
|
+
scope,
|
|
8011
|
+
tombstone,
|
|
8012
|
+
target,
|
|
8013
|
+
pending?.artifactsRemovedAttempt ??
|
|
8014
|
+
active.artifactsRemovedAttempt ??
|
|
8015
|
+
pending?.attempt ??
|
|
8016
|
+
active.attempt,
|
|
8017
|
+
)
|
|
8018
|
+
? { artifactsRemoved: true as const }
|
|
8019
|
+
: {}),
|
|
8020
|
+
},
|
|
8021
|
+
scope,
|
|
8022
|
+
target,
|
|
8023
|
+
);
|
|
8024
|
+
if (deletion.kind === "artifacts_removed") {
|
|
8025
|
+
await publishManagedGcArtifactsRemoved(scope, target, lock);
|
|
8026
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
8027
|
+
ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
|
|
8028
|
+
if (ownerProgress.state === "pending")
|
|
8029
|
+
return {
|
|
8030
|
+
kind: "cleanup_pending",
|
|
8031
|
+
tombstonePath: tombstone,
|
|
8032
|
+
phase: "artifacts",
|
|
8033
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
8034
|
+
};
|
|
8035
|
+
active = { ...active, artifactsRemovedAttempt: active.attempt };
|
|
8036
|
+
const refreshedTarget = validateCandidateForScope(scope, target);
|
|
8037
|
+
if (!refreshedTarget) throw new Error("source_changed");
|
|
8038
|
+
deletion = await deleteSessionVerifiedWithFence(
|
|
8039
|
+
"direct",
|
|
8040
|
+
"transcript-after-artifacts-removed",
|
|
8041
|
+
lock,
|
|
8042
|
+
{
|
|
8043
|
+
sessionsRoot: scope.sessionsRoot,
|
|
8044
|
+
transcriptPath: target.path,
|
|
8045
|
+
sessionId: target.sessionId,
|
|
8046
|
+
cwd: target.cwd,
|
|
8047
|
+
transcriptIdentity: refreshedTarget.identity,
|
|
8048
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
8049
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
8050
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
8051
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
8052
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
8053
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
8054
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
8055
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
8056
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
8057
|
+
artifactsRemoved: true,
|
|
8058
|
+
},
|
|
8059
|
+
scope,
|
|
8060
|
+
target,
|
|
8061
|
+
);
|
|
4804
8062
|
}
|
|
4805
8063
|
if (deletion.kind === "cleanup_pending") {
|
|
8064
|
+
if (deletion.phase === "task_artifact_owner") {
|
|
8065
|
+
ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
8066
|
+
return {
|
|
8067
|
+
kind: "cleanup_pending",
|
|
8068
|
+
tombstonePath: tombstone,
|
|
8069
|
+
phase: "artifacts",
|
|
8070
|
+
message: ownerProgress.message ?? deletion.error.message,
|
|
8071
|
+
};
|
|
8072
|
+
}
|
|
4806
8073
|
assertAuthorizedCleanupPending(target, active, deletion);
|
|
4807
8074
|
const retry = nextCleanupReceipt(target, active);
|
|
4808
8075
|
let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
|
|
@@ -4836,7 +8103,16 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4836
8103
|
phase: deletion.phase,
|
|
4837
8104
|
message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
4838
8105
|
};
|
|
8106
|
+
await publishManagedGcArtifactsRemoved(scope, target, lock);
|
|
4839
8107
|
await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
|
|
8108
|
+
ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
|
|
8109
|
+
if (ownerProgress.state === "pending")
|
|
8110
|
+
return {
|
|
8111
|
+
kind: "cleanup_pending",
|
|
8112
|
+
tombstonePath: tombstone,
|
|
8113
|
+
phase: "artifacts",
|
|
8114
|
+
message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
|
|
8115
|
+
};
|
|
4840
8116
|
pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
|
|
4841
8117
|
const retainedProof = cleanupArtifactsRemovedReceipt(
|
|
4842
8118
|
tombstone,
|
|
@@ -4872,6 +8148,8 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4872
8148
|
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
4873
8149
|
artifactsRemoved: true,
|
|
4874
8150
|
},
|
|
8151
|
+
scope,
|
|
8152
|
+
target,
|
|
4875
8153
|
() => {
|
|
4876
8154
|
if (
|
|
4877
8155
|
!cleanupArtifactsRemoved(
|
|
@@ -4884,6 +8162,15 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4884
8162
|
throw new Error("durability_failed");
|
|
4885
8163
|
},
|
|
4886
8164
|
);
|
|
8165
|
+
if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
|
|
8166
|
+
ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
|
|
8167
|
+
return {
|
|
8168
|
+
kind: "cleanup_pending",
|
|
8169
|
+
tombstonePath: tombstone,
|
|
8170
|
+
phase: "artifacts",
|
|
8171
|
+
message: ownerProgress.message ?? deletion.error.message,
|
|
8172
|
+
};
|
|
8173
|
+
}
|
|
4887
8174
|
if (deletion.kind === "cleanup_pending") {
|
|
4888
8175
|
if (
|
|
4889
8176
|
deletion.phase !== "transcript" ||
|
|
@@ -4914,12 +8201,42 @@ async function deleteManagedSessionCandidateInternal(
|
|
|
4914
8201
|
return {
|
|
4915
8202
|
kind: "cleanup_pending",
|
|
4916
8203
|
tombstonePath: tombstone,
|
|
4917
|
-
phase: deletion.phase,
|
|
4918
|
-
message:
|
|
8204
|
+
phase: ownerProgress.state === "payload_retired" ? "artifacts" : deletion.phase,
|
|
8205
|
+
message:
|
|
8206
|
+
ownerProgress.state === "payload_retired"
|
|
8207
|
+
? "task_artifact_owner_namespace_cleanup_pending"
|
|
8208
|
+
: deletion.error.message,
|
|
4919
8209
|
};
|
|
4920
8210
|
// A retained transcript quarantine proves canonical absence and remains
|
|
4921
8211
|
// identity-bound in the durable pending receipt.
|
|
4922
8212
|
}
|
|
8213
|
+
if (deletion.kind === "deleted")
|
|
8214
|
+
await persistManagedOwnerTranscriptDeletion(scope, tombstone, target, active, lock, deletion);
|
|
8215
|
+
const ownerDisposition = await taskArtifactOwnerTranscriptResult(scope, target, lock);
|
|
8216
|
+
if (ownerDisposition === "payload_retired")
|
|
8217
|
+
return {
|
|
8218
|
+
kind: "cleanup_pending",
|
|
8219
|
+
tombstonePath: tombstone,
|
|
8220
|
+
phase: "artifacts",
|
|
8221
|
+
message: "task_artifact_owner_namespace_cleanup_pending",
|
|
8222
|
+
};
|
|
8223
|
+
if (target.taskArtifactOwnerDeletionEvidence && ownerDisposition !== "retired")
|
|
8224
|
+
return {
|
|
8225
|
+
kind: "cleanup_pending",
|
|
8226
|
+
tombstonePath: tombstone,
|
|
8227
|
+
phase: "artifacts",
|
|
8228
|
+
message: "task_artifact_owner_retirement_pending",
|
|
8229
|
+
};
|
|
8230
|
+
if (
|
|
8231
|
+
target.taskArtifactOwnerDeletionEvidence &&
|
|
8232
|
+
!pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted
|
|
8233
|
+
)
|
|
8234
|
+
return {
|
|
8235
|
+
kind: "cleanup_pending",
|
|
8236
|
+
tombstonePath: tombstone,
|
|
8237
|
+
phase: "transcript",
|
|
8238
|
+
message: "task_artifact_owner_transcript_deletion_unverified",
|
|
8239
|
+
};
|
|
4923
8240
|
fsyncManagedParent(target.path);
|
|
4924
8241
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4925
8242
|
}
|