@gajae-code/coding-agent 0.11.7 → 0.11.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 +73 -1
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/keybindings.d.ts +33 -3
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/settings-schema.d.ts +15 -2
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +50 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commands/sdk.ts +2 -2
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +2 -3
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +78 -4
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/state-writer.ts +166 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/internal-urls/docs-index.generated.ts +8 -6
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +20 -4
- package/src/modes/components/welcome.ts +32 -20
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +70 -36
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +7 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/json-rpc.ts +7 -4
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +20 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +27 -2
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +853 -112
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +138 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +998 -133
- package/src/session/internal/managed-session-storage.ts +268 -203
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +406 -360
- package/src/session/session-storage.ts +151 -11
- package/src/skill-state/active-state.ts +10 -0
- package/src/task/executor.ts +13 -5
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -88
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
FileSessionStorage,
|
|
15
15
|
type NativeDirectoryTreeSnapshot,
|
|
16
16
|
type SessionStorageFileIdentity,
|
|
17
|
+
type VerifiedSessionDeleteResult,
|
|
17
18
|
} from "../session-storage";
|
|
18
19
|
import {
|
|
19
20
|
acquireManagedLock,
|
|
@@ -23,11 +24,16 @@ import {
|
|
|
23
24
|
copyManagedFileNoReplace,
|
|
24
25
|
ensureManagedDirectory,
|
|
25
26
|
fsyncManagedArtifactTree,
|
|
27
|
+
MANAGED_ARTIFACT_COPY_BATCH_SIZE,
|
|
28
|
+
MANAGED_ARTIFACT_MAX_FILES,
|
|
29
|
+
MANAGED_ARTIFACT_MAX_TOTAL_BYTES,
|
|
26
30
|
type ManagedDirectoryRoot,
|
|
31
|
+
type ManagedFileSnapshot,
|
|
32
|
+
ManagedPublishError,
|
|
27
33
|
ManagedSessionDescendantStore,
|
|
28
34
|
type ManagedSessionSecurityPolicy,
|
|
29
35
|
type ManagedStorageLock,
|
|
30
|
-
|
|
36
|
+
prepareManagedDirectoryRoot,
|
|
31
37
|
publishManagedFileNoReplace,
|
|
32
38
|
publishManagedTombstone,
|
|
33
39
|
retainManagedDirectoryAuthority,
|
|
@@ -63,13 +69,12 @@ export interface ManagedCandidateWriteAuthority {
|
|
|
63
69
|
readonly retainedDirectory?: string;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof
|
|
72
|
+
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof prepareManagedDirectoryRoot>>();
|
|
67
73
|
const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
|
|
68
74
|
const managedDirectoryAuthorities = new WeakMap<ManagedScope, native.RecoveryFsRoot | undefined>();
|
|
69
75
|
const boundManagedWriteAuthorities = new WeakMap<ManagedScope, ManagedCandidateWriteAuthority>();
|
|
70
76
|
|
|
71
77
|
function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandidateWriteAuthority): void {
|
|
72
|
-
assertManagedDirectoryRoot(authority.rootAuthority);
|
|
73
78
|
if (
|
|
74
79
|
authority.retainedAuthority &&
|
|
75
80
|
authority.retainedDirectory !== undefined &&
|
|
@@ -79,11 +84,27 @@ function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandid
|
|
|
79
84
|
authority: authority.retainedAuthority,
|
|
80
85
|
authorityBaseDir: scope.directoryPath,
|
|
81
86
|
}).assertBound();
|
|
87
|
+
} else {
|
|
88
|
+
assertManagedDirectoryRoot(authority.rootAuthority);
|
|
82
89
|
}
|
|
83
90
|
managedRoots.set(scope, authority.rootAuthority);
|
|
84
91
|
boundManagedWriteAuthorities.set(scope, authority);
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
/** A prepared scope is a retained authority boundary, not a path that may be re-adopted. */
|
|
95
|
+
function assertRetainedManagedDirectoryIdentity(scope: ManagedScope): void {
|
|
96
|
+
const expected = managedDirectoryIdentities.get(scope);
|
|
97
|
+
if (!expected) return;
|
|
98
|
+
const current = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
99
|
+
if (
|
|
100
|
+
!current.isDirectory() ||
|
|
101
|
+
current.isSymbolicLink() ||
|
|
102
|
+
current.dev !== expected.dev ||
|
|
103
|
+
current.ino !== expected.ino
|
|
104
|
+
)
|
|
105
|
+
throw new Error("Managed session directory changed");
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
export function managedDirectoryAuthorityForScope(scope: ManagedScope): native.RecoveryFsRoot | undefined {
|
|
88
109
|
if (!managedDirectoryAuthorities.has(scope)) throw new Error("Managed session directory authority was not prepared");
|
|
89
110
|
return managedDirectoryAuthorities.get(scope);
|
|
@@ -97,21 +118,24 @@ export function managedDirectoryIdentityForScope(scope: ManagedScope): { dev: bi
|
|
|
97
118
|
|
|
98
119
|
function configuredRootPath(scope: ManagedScope): string {
|
|
99
120
|
let candidate = pathIsWithin(scope.agentDir, scope.sessionsRoot) ? scope.agentDir : path.dirname(scope.sessionsRoot);
|
|
121
|
+
const suffix: string[] = [];
|
|
100
122
|
for (;;) {
|
|
101
123
|
try {
|
|
102
124
|
const stat = fs.lstatSync(candidate);
|
|
103
125
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe configured root: ${candidate}`);
|
|
104
|
-
|
|
126
|
+
const canonical = fs.realpathSync.native(candidate);
|
|
127
|
+
return suffix.length === 0 ? canonical : path.join(canonical, ...suffix);
|
|
105
128
|
} catch (error) {
|
|
106
129
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
107
130
|
const parent = path.dirname(candidate);
|
|
108
131
|
if (parent === candidate) throw new Error("Configured managed root is unavailable.");
|
|
132
|
+
suffix.unshift(path.basename(candidate));
|
|
109
133
|
candidate = parent;
|
|
110
134
|
}
|
|
111
135
|
}
|
|
112
136
|
}
|
|
113
137
|
|
|
114
|
-
function scopeRoot(scope: ManagedScope) {
|
|
138
|
+
function scopeRoot(scope: ManagedScope, policy: ManagedSessionSecurityPolicy = "default") {
|
|
115
139
|
const bound = boundManagedWriteAuthorities.get(scope);
|
|
116
140
|
if (bound) {
|
|
117
141
|
bindManagedWriteAuthority(scope, bound);
|
|
@@ -119,7 +143,7 @@ function scopeRoot(scope: ManagedScope) {
|
|
|
119
143
|
}
|
|
120
144
|
const retained = managedRoots.get(scope);
|
|
121
145
|
if (retained) return retained;
|
|
122
|
-
const root =
|
|
146
|
+
const root = prepareManagedDirectoryRoot(configuredRootPath(scope), policy);
|
|
123
147
|
managedRoots.set(scope, root);
|
|
124
148
|
return root;
|
|
125
149
|
}
|
|
@@ -137,11 +161,20 @@ export type ManagedScopeErrorCode =
|
|
|
137
161
|
| "network_unsupported"
|
|
138
162
|
| "sessions_root_unavailable"
|
|
139
163
|
| "binding_conflict"
|
|
140
|
-
| "binding_invalid"
|
|
164
|
+
| "binding_invalid"
|
|
165
|
+
| "atomic_unavailable"
|
|
166
|
+
| "invalid_request"
|
|
167
|
+
| "durability_failed"
|
|
168
|
+
| "durability_not_provable";
|
|
141
169
|
|
|
142
170
|
export type ManagedScopeResolution =
|
|
143
171
|
| { kind: "resolved"; scope: ManagedScope }
|
|
144
|
-
| {
|
|
172
|
+
| {
|
|
173
|
+
kind: "error";
|
|
174
|
+
code: ManagedScopeErrorCode;
|
|
175
|
+
message: string;
|
|
176
|
+
cause?: { readonly classification: string; readonly diagnostic?: string };
|
|
177
|
+
};
|
|
145
178
|
|
|
146
179
|
export interface ManagedCandidate {
|
|
147
180
|
sessionId: string;
|
|
@@ -169,7 +202,11 @@ export type ManagedOpenFailure =
|
|
|
169
202
|
| "destination_conflict"
|
|
170
203
|
| "source_changed"
|
|
171
204
|
| "unsafe_artifacts"
|
|
205
|
+
| "artifact_capacity_exceeded"
|
|
172
206
|
| "durability_failed"
|
|
207
|
+
| "atomic_unavailable"
|
|
208
|
+
| "invalid_request"
|
|
209
|
+
| "durability_not_provable"
|
|
173
210
|
| "migration_retired"
|
|
174
211
|
| "legacy_migration_disabled"
|
|
175
212
|
| "managed_storage_unsupported";
|
|
@@ -181,6 +218,7 @@ export type ManagedOpenCandidateResult =
|
|
|
181
218
|
export type ManagedDeleteCandidateResult =
|
|
182
219
|
| { kind: "deleted"; tombstonePath: string }
|
|
183
220
|
| { kind: "already_deleted"; tombstonePath: string }
|
|
221
|
+
| { kind: "cleanup_pending"; tombstonePath: string; phase: "artifacts" | "transcript"; message: string }
|
|
184
222
|
| { kind: "error"; code: ManagedOpenFailure; message: string };
|
|
185
223
|
|
|
186
224
|
type NativeIdentity =
|
|
@@ -535,6 +573,63 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
535
573
|
}
|
|
536
574
|
}
|
|
537
575
|
|
|
576
|
+
export function canonicalBindingOpenFlags(platform: NodeJS.Platform = process.platform): number {
|
|
577
|
+
return (platform === "win32" ? fs.constants.O_RDWR : fs.constants.O_RDONLY) | fs.constants.O_NOFOLLOW;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function fsyncCanonicalBinding(bindingPath: string, expected: string): void {
|
|
581
|
+
let captured: ManagedFileSnapshot;
|
|
582
|
+
try {
|
|
583
|
+
captured = captureManagedFileNoFollow(bindingPath);
|
|
584
|
+
} catch {
|
|
585
|
+
throw new Error("binding_invalid");
|
|
586
|
+
}
|
|
587
|
+
if (captured.bytes.toString("utf8") !== expected) throw new Error("binding_invalid");
|
|
588
|
+
let descriptor: number | undefined;
|
|
589
|
+
try {
|
|
590
|
+
// Bun on Windows rejects fsync on a read-only file descriptor with EPERM.
|
|
591
|
+
// The managed binding is owner-writable, so reopen it read/write only for the durability fence.
|
|
592
|
+
descriptor = fs.openSync(bindingPath, canonicalBindingOpenFlags());
|
|
593
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
594
|
+
if (
|
|
595
|
+
before.dev !== captured.identity.dev ||
|
|
596
|
+
before.ino !== captured.identity.ino ||
|
|
597
|
+
before.size !== BigInt(captured.identity.size) ||
|
|
598
|
+
before.mtimeNs !== captured.identity.mtimeNs
|
|
599
|
+
)
|
|
600
|
+
throw new Error("binding_invalid");
|
|
601
|
+
fs.fsyncSync(descriptor);
|
|
602
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
603
|
+
if (
|
|
604
|
+
after.dev !== captured.identity.dev ||
|
|
605
|
+
after.ino !== captured.identity.ino ||
|
|
606
|
+
after.size !== BigInt(captured.identity.size) ||
|
|
607
|
+
after.mtimeNs !== captured.identity.mtimeNs
|
|
608
|
+
)
|
|
609
|
+
throw new Error("binding_invalid");
|
|
610
|
+
} catch (error) {
|
|
611
|
+
if (error instanceof Error && error.message === "binding_invalid") throw error;
|
|
612
|
+
throw new Error("durability_failed", { cause: error });
|
|
613
|
+
} finally {
|
|
614
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
615
|
+
}
|
|
616
|
+
fsyncManagedParent(bindingPath);
|
|
617
|
+
let recaptured: ManagedFileSnapshot;
|
|
618
|
+
try {
|
|
619
|
+
recaptured = captureManagedFileNoFollow(bindingPath);
|
|
620
|
+
} catch {
|
|
621
|
+
throw new Error("binding_invalid");
|
|
622
|
+
}
|
|
623
|
+
if (
|
|
624
|
+
recaptured.bytes.toString("utf8") !== expected ||
|
|
625
|
+
recaptured.identity.dev !== captured.identity.dev ||
|
|
626
|
+
recaptured.identity.ino !== captured.identity.ino ||
|
|
627
|
+
recaptured.identity.size !== captured.identity.size ||
|
|
628
|
+
recaptured.identity.mtimeNs !== captured.identity.mtimeNs
|
|
629
|
+
)
|
|
630
|
+
throw new Error("binding_invalid");
|
|
631
|
+
}
|
|
632
|
+
|
|
538
633
|
type CandidatePreflight =
|
|
539
634
|
| { kind: "capture"; identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint } }
|
|
540
635
|
| {
|
|
@@ -673,22 +768,48 @@ export async function ensureManagedScope(
|
|
|
673
768
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
674
769
|
): Promise<ManagedScopeResolution> {
|
|
675
770
|
try {
|
|
676
|
-
|
|
771
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
772
|
+
const root = scopeRoot(scope, policy);
|
|
677
773
|
ensureManagedDirectory(scope.sessionsRoot, root, policy);
|
|
678
774
|
ensureManagedDirectory(scope.directoryPath, root, policy);
|
|
679
775
|
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
680
776
|
const binding = `${JSON.stringify(bindingFor(scope))}\n`;
|
|
777
|
+
let bindingCollision = false;
|
|
681
778
|
try {
|
|
682
779
|
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
|
|
683
780
|
} catch (error) {
|
|
684
781
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
782
|
+
bindingCollision = true;
|
|
685
783
|
}
|
|
686
|
-
|
|
784
|
+
const validated = validateExistingBinding(scope);
|
|
785
|
+
if (validated) return validated;
|
|
786
|
+
if (bindingCollision) fsyncCanonicalBinding(bindingPath, binding);
|
|
787
|
+
|
|
788
|
+
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
789
|
+
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
790
|
+
throw new Error("Managed session directory changed");
|
|
791
|
+
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
792
|
+
return { kind: "resolved", scope };
|
|
687
793
|
} catch (error) {
|
|
794
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
795
|
+
const message =
|
|
796
|
+
publication?.classification ??
|
|
797
|
+
(error instanceof Error ? error.message : "The managed scope could not be initialized.");
|
|
798
|
+
const code =
|
|
799
|
+
message === "atomic_unavailable" ||
|
|
800
|
+
message === "invalid_request" ||
|
|
801
|
+
message === "durability_failed" ||
|
|
802
|
+
message === "durability_not_provable"
|
|
803
|
+
? message
|
|
804
|
+
: "binding_invalid";
|
|
805
|
+
|
|
688
806
|
return {
|
|
689
807
|
kind: "error",
|
|
690
|
-
code
|
|
691
|
-
message
|
|
808
|
+
code,
|
|
809
|
+
message,
|
|
810
|
+
...(publication
|
|
811
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
812
|
+
: { cause: { classification: code } }),
|
|
692
813
|
};
|
|
693
814
|
}
|
|
694
815
|
}
|
|
@@ -759,7 +880,8 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
759
880
|
authority?: ManagedCandidateWriteAuthority,
|
|
760
881
|
): ManagedScopeResolution {
|
|
761
882
|
try {
|
|
762
|
-
|
|
883
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
884
|
+
const root = authority?.rootAuthority ?? scopeRoot(scope, policy);
|
|
763
885
|
if (authority) bindManagedWriteAuthority(scope, authority);
|
|
764
886
|
ensureManagedDirectory(scope.sessionsRoot, root, policy);
|
|
765
887
|
ensureManagedDirectory(scope.directoryPath, root, policy);
|
|
@@ -821,10 +943,26 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
821
943
|
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
|
|
822
944
|
return { kind: "resolved", scope };
|
|
823
945
|
} catch (error) {
|
|
946
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
947
|
+
const message =
|
|
948
|
+
publication?.classification ??
|
|
949
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
950
|
+
const code =
|
|
951
|
+
message === "atomic_unavailable" ||
|
|
952
|
+
message === "invalid_request" ||
|
|
953
|
+
message === "durability_failed" ||
|
|
954
|
+
message === "durability_not_provable"
|
|
955
|
+
? message
|
|
956
|
+
: "binding_invalid";
|
|
824
957
|
return {
|
|
825
958
|
kind: "error",
|
|
826
|
-
code
|
|
827
|
-
message
|
|
959
|
+
code,
|
|
960
|
+
message,
|
|
961
|
+
...(publication
|
|
962
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
963
|
+
: code === "binding_invalid"
|
|
964
|
+
? {}
|
|
965
|
+
: { cause: { classification: code } }),
|
|
828
966
|
};
|
|
829
967
|
}
|
|
830
968
|
}
|
|
@@ -947,7 +1085,11 @@ function expectedFailure(error: unknown): ManagedOpenFailure {
|
|
|
947
1085
|
message === "destination_conflict" ||
|
|
948
1086
|
message === "source_changed" ||
|
|
949
1087
|
message === "unsafe_artifacts" ||
|
|
1088
|
+
message === "artifact_capacity_exceeded" ||
|
|
950
1089
|
message === "durability_failed" ||
|
|
1090
|
+
message === "atomic_unavailable" ||
|
|
1091
|
+
message === "invalid_request" ||
|
|
1092
|
+
message === "durability_not_provable" ||
|
|
951
1093
|
message === "migration_retired"
|
|
952
1094
|
? message
|
|
953
1095
|
: "managed_storage_unsupported";
|
|
@@ -990,7 +1132,7 @@ function revalidatePickerConsent(
|
|
|
990
1132
|
function receiptPathFor(
|
|
991
1133
|
scope: ManagedScope,
|
|
992
1134
|
source: ManagedCandidate,
|
|
993
|
-
state: "prepared" | "published" | "committed" = "committed",
|
|
1135
|
+
state: "prepared" | "detached" | "published" | "committed" = "committed",
|
|
994
1136
|
): string {
|
|
995
1137
|
const suffix = state === "committed" ? "" : `.${state}`;
|
|
996
1138
|
return path.join(
|
|
@@ -1029,6 +1171,14 @@ function receiptMatches(
|
|
|
1029
1171
|
identity?: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown };
|
|
1030
1172
|
};
|
|
1031
1173
|
artifactManifest?: unknown;
|
|
1174
|
+
sourceArtifactQuarantine?: { role?: unknown };
|
|
1175
|
+
sourceArtifactCleanup?: {
|
|
1176
|
+
state?: unknown;
|
|
1177
|
+
role?: unknown;
|
|
1178
|
+
retainedPath?: unknown;
|
|
1179
|
+
identity?: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown };
|
|
1180
|
+
tree?: unknown;
|
|
1181
|
+
};
|
|
1032
1182
|
};
|
|
1033
1183
|
const exact = (
|
|
1034
1184
|
recorded: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown } | undefined,
|
|
@@ -1069,12 +1219,43 @@ function receiptMatches(
|
|
|
1069
1219
|
item.size >= 0
|
|
1070
1220
|
);
|
|
1071
1221
|
});
|
|
1222
|
+
const quarantine = record.sourceArtifactQuarantine;
|
|
1223
|
+
const cleanup = record.sourceArtifactCleanup;
|
|
1224
|
+
const cleanupIdentity = cleanup?.identity;
|
|
1225
|
+
const cleanupTree = artifactTreeSnapshot(cleanup?.tree);
|
|
1226
|
+
const validCleanup =
|
|
1227
|
+
cleanup === undefined ||
|
|
1228
|
+
(quarantine?.role === "detached_artifact_root" &&
|
|
1229
|
+
cleanup.state === "cleanup_pending" &&
|
|
1230
|
+
cleanup.role === "exchange_placeholder" &&
|
|
1231
|
+
typeof cleanup.retainedPath === "string" &&
|
|
1232
|
+
cleanupIdentity?.dev !== undefined &&
|
|
1233
|
+
cleanupIdentity.ino !== undefined &&
|
|
1234
|
+
cleanupIdentity.size !== undefined &&
|
|
1235
|
+
cleanupIdentity.mtimeNs !== undefined &&
|
|
1236
|
+
!!cleanupTree &&
|
|
1237
|
+
cleanupAuthorityMatches(
|
|
1238
|
+
{
|
|
1239
|
+
state: "cleanup_pending",
|
|
1240
|
+
role: "exchange_placeholder",
|
|
1241
|
+
retainedPath: cleanup.retainedPath,
|
|
1242
|
+
identity: {
|
|
1243
|
+
dev: BigInt(String(cleanupIdentity.dev)),
|
|
1244
|
+
ino: BigInt(String(cleanupIdentity.ino)),
|
|
1245
|
+
size: BigInt(String(cleanupIdentity.size)),
|
|
1246
|
+
mtimeNs: BigInt(String(cleanupIdentity.mtimeNs)),
|
|
1247
|
+
},
|
|
1248
|
+
tree: cleanupTree,
|
|
1249
|
+
},
|
|
1250
|
+
path.dirname(source.path),
|
|
1251
|
+
));
|
|
1072
1252
|
return (
|
|
1073
1253
|
record.schemaVersion === 2 &&
|
|
1074
1254
|
record.state === "committed" &&
|
|
1075
1255
|
record.policy === "copy-retain" &&
|
|
1076
1256
|
record.scope === scopeDigest(scope.platform, scope.canonicalCwd) &&
|
|
1077
1257
|
validManifest &&
|
|
1258
|
+
validCleanup &&
|
|
1078
1259
|
manifestContains(destination.path, manifest as readonly ArtifactManifestEntry[]) &&
|
|
1079
1260
|
record.source?.path === source.path &&
|
|
1080
1261
|
record.source?.sessionId === source.sessionId &&
|
|
@@ -1217,6 +1398,65 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1217
1398
|
* a returned partial result appends `pending(N + 1)` with the observed detached
|
|
1218
1399
|
* identity/path. Restart accepts only a contiguous, target-bound sequence.
|
|
1219
1400
|
*/
|
|
1401
|
+
type RetainedArtifactsRootReceipt = {
|
|
1402
|
+
path: string;
|
|
1403
|
+
identity: SessionStorageFileIdentity;
|
|
1404
|
+
tree: NativeDirectoryTreeSnapshot;
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
|
|
1408
|
+
const pathname = deterministicRemovalRoot(receipt.plannedArtifactsPath);
|
|
1409
|
+
if (!fs.existsSync(pathname)) return undefined;
|
|
1410
|
+
if (!receipt.expectedArtifactsIdentity) throw new Error("durability_failed");
|
|
1411
|
+
const identity = artifactIdentityAt(pathname);
|
|
1412
|
+
if (!identity || !sameArtifactRootIdentity(identity, receipt.expectedArtifactsIdentity))
|
|
1413
|
+
throw new Error("durability_failed");
|
|
1414
|
+
const tree = snapshotArtifactTree(pathname);
|
|
1415
|
+
return { path: pathname, identity, tree };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean {
|
|
1419
|
+
if (record.retainedArtifactsRoot === undefined) return true;
|
|
1420
|
+
if (!record.retainedArtifactsRoot || typeof record.retainedArtifactsRoot !== "object")
|
|
1421
|
+
throw new Error("durability_failed");
|
|
1422
|
+
const retained = record.retainedArtifactsRoot as Record<string, unknown>;
|
|
1423
|
+
const identity = retained.identity;
|
|
1424
|
+
const tree = artifactTreeSnapshot(retained.tree);
|
|
1425
|
+
if (
|
|
1426
|
+
typeof record.plannedArtifactsPath !== "string" ||
|
|
1427
|
+
retained.path !== deterministicRemovalRoot(record.plannedArtifactsPath) ||
|
|
1428
|
+
!identity ||
|
|
1429
|
+
typeof identity !== "object" ||
|
|
1430
|
+
Array.isArray(identity) ||
|
|
1431
|
+
!tree
|
|
1432
|
+
)
|
|
1433
|
+
throw new Error("durability_failed");
|
|
1434
|
+
const artifactIdentity = identity as Record<string, unknown>;
|
|
1435
|
+
if (
|
|
1436
|
+
typeof artifactIdentity.dev !== "string" ||
|
|
1437
|
+
typeof artifactIdentity.ino !== "string" ||
|
|
1438
|
+
typeof artifactIdentity.size !== "number" ||
|
|
1439
|
+
typeof artifactIdentity.mtimeNs !== "string" ||
|
|
1440
|
+
typeof artifactIdentity.sha256 !== "string"
|
|
1441
|
+
)
|
|
1442
|
+
throw new Error("durability_failed");
|
|
1443
|
+
if (!fs.existsSync(retained.path)) return true;
|
|
1444
|
+
const observed = artifactIdentityAt(retained.path);
|
|
1445
|
+
if (
|
|
1446
|
+
!observed ||
|
|
1447
|
+
!sameArtifactRootIdentity(observed, {
|
|
1448
|
+
dev: BigInt(artifactIdentity.dev),
|
|
1449
|
+
ino: BigInt(artifactIdentity.ino),
|
|
1450
|
+
size: artifactIdentity.size,
|
|
1451
|
+
mtimeNs: BigInt(artifactIdentity.mtimeNs),
|
|
1452
|
+
sha256: artifactIdentity.sha256,
|
|
1453
|
+
}) ||
|
|
1454
|
+
JSON.stringify(snapshotArtifactTree(retained.path)) !== JSON.stringify(tree)
|
|
1455
|
+
)
|
|
1456
|
+
throw new Error("durability_failed");
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1220
1460
|
type CleanupReceipt = {
|
|
1221
1461
|
attempt: number;
|
|
1222
1462
|
target: RetiredTarget;
|
|
@@ -1224,6 +1464,13 @@ type CleanupReceipt = {
|
|
|
1224
1464
|
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
1225
1465
|
detachedArtifactsPath?: string;
|
|
1226
1466
|
detachedTranscriptPath?: string;
|
|
1467
|
+
retainedArtifactsSuccessorPath?: string;
|
|
1468
|
+
retainedArtifactsPlaceholderPath?: string;
|
|
1469
|
+
retainedArtifactsUnknownPath?: string;
|
|
1470
|
+
retainedTranscriptSuccessorPath?: string;
|
|
1471
|
+
retainedTranscriptPlaceholderPath?: string;
|
|
1472
|
+
retainedTranscriptUnknownPath?: string;
|
|
1473
|
+
|
|
1227
1474
|
plannedArtifactsPath: string;
|
|
1228
1475
|
plannedTranscriptPath: string;
|
|
1229
1476
|
};
|
|
@@ -1257,6 +1504,24 @@ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: Cleanup
|
|
|
1257
1504
|
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
1258
1505
|
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
1259
1506
|
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
1507
|
+
...(receipt.retainedArtifactsSuccessorPath
|
|
1508
|
+
? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
|
|
1509
|
+
: {}),
|
|
1510
|
+
...(receipt.retainedArtifactsPlaceholderPath
|
|
1511
|
+
? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
|
|
1512
|
+
: {}),
|
|
1513
|
+
...(receipt.retainedArtifactsUnknownPath
|
|
1514
|
+
? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
|
|
1515
|
+
: {}),
|
|
1516
|
+
...(receipt.retainedTranscriptSuccessorPath
|
|
1517
|
+
? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
|
|
1518
|
+
: {}),
|
|
1519
|
+
...(receipt.retainedTranscriptPlaceholderPath
|
|
1520
|
+
? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
|
|
1521
|
+
: {}),
|
|
1522
|
+
...(receipt.retainedTranscriptUnknownPath
|
|
1523
|
+
? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
|
|
1524
|
+
: {}),
|
|
1260
1525
|
plannedArtifactsPath: receipt.plannedArtifactsPath,
|
|
1261
1526
|
plannedTranscriptPath: receipt.plannedTranscriptPath,
|
|
1262
1527
|
};
|
|
@@ -1277,6 +1542,7 @@ function cleanupArtifactsRemoved(
|
|
|
1277
1542
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1278
1543
|
const record = value as Record<string, unknown>;
|
|
1279
1544
|
const recorded = record.target as Record<string, unknown> | undefined;
|
|
1545
|
+
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
1280
1546
|
return (
|
|
1281
1547
|
record.schemaVersion === 2 &&
|
|
1282
1548
|
record.state === "artifacts_removed" &&
|
|
@@ -1285,9 +1551,17 @@ function cleanupArtifactsRemoved(
|
|
|
1285
1551
|
record.attempt === attempt &&
|
|
1286
1552
|
recorded?.path === target.path &&
|
|
1287
1553
|
recorded.sessionId === target.sessionId &&
|
|
1288
|
-
recorded.cwd === target.cwd
|
|
1554
|
+
recorded.cwd === target.cwd &&
|
|
1555
|
+
identity?.canonicalPath === target.identity.canonicalPath &&
|
|
1556
|
+
identity.dev === String(target.identity.dev) &&
|
|
1557
|
+
identity.ino === String(target.identity.ino) &&
|
|
1558
|
+
identity.size === target.identity.size &&
|
|
1559
|
+
identity.mtimeNs === String(target.identity.mtimeNs) &&
|
|
1560
|
+
identity.sha256 === target.identity.sha256 &&
|
|
1561
|
+
retainedArtifactsRootMatches(record)
|
|
1289
1562
|
);
|
|
1290
|
-
} catch {
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
if ((error as Error).message === "durability_failed") throw error;
|
|
1291
1565
|
return false;
|
|
1292
1566
|
}
|
|
1293
1567
|
}
|
|
@@ -1298,9 +1572,14 @@ async function publishCleanupArtifactsRemoved(
|
|
|
1298
1572
|
receipt: CleanupReceipt,
|
|
1299
1573
|
lock: ManagedStorageLock,
|
|
1300
1574
|
): Promise<void> {
|
|
1575
|
+
const retainedArtifactsRoot = retainedArtifactsRootReceipt(receipt);
|
|
1301
1576
|
await publishManagedTombstone(
|
|
1302
1577
|
cleanupReceiptPath(tombstone, receipt.target, "artifacts_removed", receipt.attempt),
|
|
1303
|
-
{
|
|
1578
|
+
{
|
|
1579
|
+
...cleanupReceipt(scope, tombstone, receipt),
|
|
1580
|
+
state: "artifacts_removed",
|
|
1581
|
+
...(retainedArtifactsRoot ? { retainedArtifactsRoot } : {}),
|
|
1582
|
+
},
|
|
1304
1583
|
lock.assertOwned,
|
|
1305
1584
|
).catch(error => {
|
|
1306
1585
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
@@ -1317,6 +1596,14 @@ function isQuarantinePath(target: RetiredTarget, pathname: unknown): pathname is
|
|
|
1317
1596
|
);
|
|
1318
1597
|
}
|
|
1319
1598
|
|
|
1599
|
+
function isRetainedNativePath(target: RetiredTarget, pathname: unknown): pathname is string {
|
|
1600
|
+
return (
|
|
1601
|
+
typeof pathname === "string" &&
|
|
1602
|
+
path.dirname(pathname) === path.dirname(target.path) &&
|
|
1603
|
+
path.basename(pathname).startsWith(".gjc-")
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1320
1607
|
function deterministicRemovalRoot(plannedRoot: string): string {
|
|
1321
1608
|
return `${plannedRoot}.removing`;
|
|
1322
1609
|
}
|
|
@@ -1328,17 +1615,44 @@ function isAuthorizedArtifactRoot(target: RetiredTarget, plannedRoot: string, pa
|
|
|
1328
1615
|
);
|
|
1329
1616
|
}
|
|
1330
1617
|
|
|
1331
|
-
function
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1618
|
+
function transcriptRootMatchesTarget(pathname: string, target: RetiredTarget): boolean {
|
|
1619
|
+
try {
|
|
1620
|
+
const observed = captureManagedFileNoFollow(pathname);
|
|
1621
|
+
const digest = createHash("sha256").update(observed.bytes).digest("hex");
|
|
1622
|
+
return (
|
|
1623
|
+
observed.identity.dev === target.identity.dev &&
|
|
1624
|
+
observed.identity.ino === target.identity.ino &&
|
|
1625
|
+
observed.identity.size === target.identity.size &&
|
|
1626
|
+
observed.identity.mtimeNs === target.identity.mtimeNs &&
|
|
1627
|
+
digest === target.identity.sha256
|
|
1628
|
+
);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
1631
|
+
throw new Error("durability_failed");
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function cleanupRootsAbsent(tombstone: string, target: RetiredTarget, pending: CleanupReceipt): boolean {
|
|
1337
1636
|
const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
|
|
1637
|
+
const activeTranscriptRoots = new Set(
|
|
1638
|
+
[pending.plannedTranscriptPath, pending.detachedTranscriptPath].filter((pathname): pathname is string =>
|
|
1639
|
+
isQuarantinePath(target, pathname),
|
|
1640
|
+
),
|
|
1641
|
+
);
|
|
1338
1642
|
const roots = new Set<string>([
|
|
1643
|
+
target.path,
|
|
1644
|
+
target.path.slice(0, -6),
|
|
1339
1645
|
pending.plannedArtifactsPath,
|
|
1340
1646
|
deterministicRemovalRoot(pending.plannedArtifactsPath),
|
|
1341
1647
|
...(pending.detachedArtifactsPath ? [pending.detachedArtifactsPath] : []),
|
|
1648
|
+
...[
|
|
1649
|
+
pending.retainedArtifactsSuccessorPath,
|
|
1650
|
+
pending.retainedArtifactsPlaceholderPath,
|
|
1651
|
+
pending.retainedArtifactsUnknownPath,
|
|
1652
|
+
pending.retainedTranscriptSuccessorPath,
|
|
1653
|
+
pending.retainedTranscriptPlaceholderPath,
|
|
1654
|
+
pending.retainedTranscriptUnknownPath,
|
|
1655
|
+
].filter((pathname): pathname is string => isRetainedNativePath(target, pathname)),
|
|
1342
1656
|
]);
|
|
1343
1657
|
for (const name of fs.readdirSync(path.dirname(tombstone))) {
|
|
1344
1658
|
if (!name.startsWith(prefix) || !name.endsWith(".json")) continue;
|
|
@@ -1347,26 +1661,91 @@ function artifactRootsAbsent(
|
|
|
1347
1661
|
) as {
|
|
1348
1662
|
plannedArtifactsPath?: unknown;
|
|
1349
1663
|
detachedArtifactsPath?: unknown;
|
|
1664
|
+
plannedTranscriptPath?: unknown;
|
|
1665
|
+
detachedTranscriptPath?: unknown;
|
|
1666
|
+
retainedArtifactsSuccessorPath?: unknown;
|
|
1667
|
+
retainedArtifactsPlaceholderPath?: unknown;
|
|
1668
|
+
retainedArtifactsUnknownPath?: unknown;
|
|
1669
|
+
retainedTranscriptSuccessorPath?: unknown;
|
|
1670
|
+
retainedTranscriptPlaceholderPath?: unknown;
|
|
1671
|
+
retainedTranscriptUnknownPath?: unknown;
|
|
1350
1672
|
};
|
|
1351
1673
|
if (isQuarantinePath(target, record.plannedArtifactsPath)) {
|
|
1352
1674
|
roots.add(record.plannedArtifactsPath);
|
|
1353
1675
|
roots.add(deterministicRemovalRoot(record.plannedArtifactsPath));
|
|
1354
1676
|
}
|
|
1355
1677
|
if (isQuarantinePath(target, record.detachedArtifactsPath)) roots.add(record.detachedArtifactsPath);
|
|
1678
|
+
for (const pathname of [record.plannedTranscriptPath, record.detachedTranscriptPath]) {
|
|
1679
|
+
if (isQuarantinePath(target, pathname) && !activeTranscriptRoots.has(pathname)) roots.add(pathname);
|
|
1680
|
+
}
|
|
1681
|
+
for (const pathname of [
|
|
1682
|
+
record.retainedArtifactsSuccessorPath,
|
|
1683
|
+
record.retainedArtifactsPlaceholderPath,
|
|
1684
|
+
record.retainedArtifactsUnknownPath,
|
|
1685
|
+
record.retainedTranscriptSuccessorPath,
|
|
1686
|
+
record.retainedTranscriptPlaceholderPath,
|
|
1687
|
+
record.retainedTranscriptUnknownPath,
|
|
1688
|
+
]) {
|
|
1689
|
+
if (isRetainedNativePath(target, pathname)) roots.add(pathname);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
for (const pathname of roots) {
|
|
1693
|
+
try {
|
|
1694
|
+
fs.lstatSync(pathname);
|
|
1695
|
+
throw new Error("durability_failed");
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1698
|
+
throw error;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
for (const pathname of activeTranscriptRoots) {
|
|
1702
|
+
try {
|
|
1703
|
+
fs.lstatSync(pathname);
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1706
|
+
throw error;
|
|
1707
|
+
}
|
|
1708
|
+
if (!transcriptRootMatchesTarget(pathname, target)) throw new Error("durability_failed");
|
|
1709
|
+
return false;
|
|
1356
1710
|
}
|
|
1357
|
-
return
|
|
1711
|
+
return true;
|
|
1358
1712
|
}
|
|
1359
1713
|
|
|
1360
1714
|
function sameArtifactRootIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
|
|
1361
1715
|
return left.dev === right.dev && left.ino === right.ino;
|
|
1362
1716
|
}
|
|
1363
1717
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1718
|
+
function sameArtifactTreeSnapshot(left: NativeDirectoryTreeSnapshot, right: NativeDirectoryTreeSnapshot): boolean {
|
|
1719
|
+
if (left.rootDev !== right.rootDev || left.rootIno !== right.rootIno || left.entries.length !== right.entries.length)
|
|
1720
|
+
return false;
|
|
1721
|
+
const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string =>
|
|
1722
|
+
JSON.stringify([entry.relativePath, entry.kind, entry.dev, entry.ino, entry.size, entry.mtimeNs, entry.sha256]);
|
|
1723
|
+
const leftEntries = left.entries.map(entryKey).sort();
|
|
1724
|
+
const rightEntries = right.entries.map(entryKey).sort();
|
|
1725
|
+
return leftEntries.every((entry, index) => entry === rightEntries[index]);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
1729
|
+
if (!pending.detachedArtifactsPath) return;
|
|
1730
|
+
if (!pending.expectedArtifactsIdentity || !pending.expectedArtifactsTree) throw new Error("durability_failed");
|
|
1731
|
+
try {
|
|
1732
|
+
fs.lstatSync(pending.detachedArtifactsPath);
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
1735
|
+
throw new Error("durability_failed");
|
|
1736
|
+
}
|
|
1737
|
+
const observed = artifactIdentityAt(pending.detachedArtifactsPath);
|
|
1738
|
+
if (
|
|
1739
|
+
!observed ||
|
|
1740
|
+
!sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
|
|
1741
|
+
!sameArtifactTreeSnapshot(snapshotArtifactTree(pending.detachedArtifactsPath), pending.expectedArtifactsTree)
|
|
1742
|
+
)
|
|
1743
|
+
throw new Error("binding_invalid");
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1369
1746
|
function probePlannedCleanupDetach(target: RetiredTarget, pending: CleanupReceipt): CleanupReceipt {
|
|
1747
|
+
assertRetainedArtifactsAuthority(pending);
|
|
1748
|
+
|
|
1370
1749
|
let detachedArtifactsPath = pending.detachedArtifactsPath;
|
|
1371
1750
|
let detachedTranscriptPath = pending.detachedTranscriptPath;
|
|
1372
1751
|
for (const pathname of [pending.plannedArtifactsPath, deterministicRemovalRoot(pending.plannedArtifactsPath)]) {
|
|
@@ -1430,6 +1809,16 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
1430
1809
|
})
|
|
1431
1810
|
)
|
|
1432
1811
|
return undefined;
|
|
1812
|
+
const roots = snapshot.entries.filter(entry => {
|
|
1813
|
+
const item = entry as Record<string, unknown>;
|
|
1814
|
+
return (
|
|
1815
|
+
item.relativePath === "" &&
|
|
1816
|
+
item.kind === "directory" &&
|
|
1817
|
+
item.dev === snapshot.rootDev &&
|
|
1818
|
+
item.ino === snapshot.rootIno
|
|
1819
|
+
);
|
|
1820
|
+
});
|
|
1821
|
+
if (roots.length !== 1) return undefined;
|
|
1433
1822
|
return snapshot as unknown as NativeDirectoryTreeSnapshot;
|
|
1434
1823
|
}
|
|
1435
1824
|
|
|
@@ -1481,7 +1870,19 @@ function pendingCleanupReceipt(
|
|
|
1481
1870
|
plannedPaths.has(record.plannedArtifactsPath as string) ||
|
|
1482
1871
|
plannedPaths.has(record.plannedTranscriptPath as string) ||
|
|
1483
1872
|
(record.detachedArtifactsPath !== undefined && !isQuarantinePath(target, record.detachedArtifactsPath)) ||
|
|
1484
|
-
(record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath))
|
|
1873
|
+
(record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath)) ||
|
|
1874
|
+
(record.retainedArtifactsSuccessorPath !== undefined &&
|
|
1875
|
+
!isRetainedNativePath(target, record.retainedArtifactsSuccessorPath)) ||
|
|
1876
|
+
(record.retainedArtifactsPlaceholderPath !== undefined &&
|
|
1877
|
+
!isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) ||
|
|
1878
|
+
(record.retainedArtifactsUnknownPath !== undefined &&
|
|
1879
|
+
!isRetainedNativePath(target, record.retainedArtifactsUnknownPath)) ||
|
|
1880
|
+
(record.retainedTranscriptSuccessorPath !== undefined &&
|
|
1881
|
+
!isRetainedNativePath(target, record.retainedTranscriptSuccessorPath)) ||
|
|
1882
|
+
(record.retainedTranscriptPlaceholderPath !== undefined &&
|
|
1883
|
+
!isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
|
|
1884
|
+
(record.retainedTranscriptUnknownPath !== undefined &&
|
|
1885
|
+
!isRetainedNativePath(target, record.retainedTranscriptUnknownPath))
|
|
1485
1886
|
)
|
|
1486
1887
|
throw new Error("durability_failed");
|
|
1487
1888
|
const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
|
|
@@ -1504,7 +1905,7 @@ function pendingCleanupReceipt(
|
|
|
1504
1905
|
const expectedArtifactsTree =
|
|
1505
1906
|
record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
|
|
1506
1907
|
if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
|
|
1507
|
-
|
|
1908
|
+
|
|
1508
1909
|
if (
|
|
1509
1910
|
latest &&
|
|
1510
1911
|
((record.detachedArtifactsPath !== undefined &&
|
|
@@ -1523,6 +1924,12 @@ function pendingCleanupReceipt(
|
|
|
1523
1924
|
expectedArtifactsTree,
|
|
1524
1925
|
detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
|
|
1525
1926
|
detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
|
|
1927
|
+
retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
|
|
1928
|
+
retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
|
|
1929
|
+
retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
|
|
1930
|
+
retainedTranscriptSuccessorPath: record.retainedTranscriptSuccessorPath as string | undefined,
|
|
1931
|
+
retainedTranscriptPlaceholderPath: record.retainedTranscriptPlaceholderPath as string | undefined,
|
|
1932
|
+
retainedTranscriptUnknownPath: record.retainedTranscriptUnknownPath as string | undefined,
|
|
1526
1933
|
plannedArtifactsPath: record.plannedArtifactsPath as string,
|
|
1527
1934
|
plannedTranscriptPath: record.plannedTranscriptPath as string,
|
|
1528
1935
|
};
|
|
@@ -1572,7 +1979,13 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
|
|
|
1572
1979
|
expectedArtifactsIdentity,
|
|
1573
1980
|
expectedArtifactsTree,
|
|
1574
1981
|
detachedArtifactsPath: pending?.detachedArtifactsPath,
|
|
1982
|
+
retainedArtifactsSuccessorPath: pending?.retainedArtifactsSuccessorPath,
|
|
1983
|
+
retainedArtifactsPlaceholderPath: pending?.retainedArtifactsPlaceholderPath,
|
|
1984
|
+
retainedArtifactsUnknownPath: pending?.retainedArtifactsUnknownPath,
|
|
1575
1985
|
detachedTranscriptPath: pending?.detachedTranscriptPath,
|
|
1986
|
+
retainedTranscriptSuccessorPath: pending?.retainedTranscriptSuccessorPath,
|
|
1987
|
+
retainedTranscriptPlaceholderPath: pending?.retainedTranscriptPlaceholderPath,
|
|
1988
|
+
retainedTranscriptUnknownPath: pending?.retainedTranscriptUnknownPath,
|
|
1576
1989
|
plannedArtifactsPath: path.join(directory, `.gjc-delete-${operation}-artifacts-${attempt}`),
|
|
1577
1990
|
plannedTranscriptPath: path.join(directory, `.gjc-delete-${operation}-transcript-${attempt}`),
|
|
1578
1991
|
};
|
|
@@ -1585,6 +1998,36 @@ function requiresFreshCleanupPlan(pending: CleanupReceipt): boolean {
|
|
|
1585
1998
|
);
|
|
1586
1999
|
}
|
|
1587
2000
|
|
|
2001
|
+
/** Evidence-carrier for a verified `cleanup_pending` deletion, bound to the active plan. */
|
|
2002
|
+
function cleanupPendingEvidence(
|
|
2003
|
+
retry: CleanupReceipt,
|
|
2004
|
+
active: CleanupReceipt,
|
|
2005
|
+
deletion: Extract<VerifiedSessionDeleteResult, { kind: "cleanup_pending" }>,
|
|
2006
|
+
): CleanupReceipt {
|
|
2007
|
+
return {
|
|
2008
|
+
...retry,
|
|
2009
|
+
expectedArtifactsIdentity:
|
|
2010
|
+
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
2011
|
+
expectedArtifactsTree: deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2012
|
+
detachedArtifactsPath:
|
|
2013
|
+
deletion.phase === "artifacts" ? deletion.detachedArtifactsPath : active.detachedArtifactsPath,
|
|
2014
|
+
detachedTranscriptPath:
|
|
2015
|
+
deletion.phase === "transcript" ? deletion.detachedTranscriptPath : active.detachedTranscriptPath,
|
|
2016
|
+
retainedArtifactsSuccessorPath:
|
|
2017
|
+
deletion.phase === "artifacts" ? deletion.retainedSuccessorPath : active.retainedArtifactsSuccessorPath,
|
|
2018
|
+
retainedArtifactsPlaceholderPath:
|
|
2019
|
+
deletion.phase === "artifacts" ? deletion.retainedPlaceholderPath : active.retainedArtifactsPlaceholderPath,
|
|
2020
|
+
retainedArtifactsUnknownPath:
|
|
2021
|
+
deletion.phase === "artifacts" ? deletion.retainedUnknownPath : active.retainedArtifactsUnknownPath,
|
|
2022
|
+
retainedTranscriptSuccessorPath:
|
|
2023
|
+
deletion.phase === "transcript" ? deletion.retainedSuccessorPath : active.retainedTranscriptSuccessorPath,
|
|
2024
|
+
retainedTranscriptPlaceholderPath:
|
|
2025
|
+
deletion.phase === "transcript" ? deletion.retainedPlaceholderPath : active.retainedTranscriptPlaceholderPath,
|
|
2026
|
+
retainedTranscriptUnknownPath:
|
|
2027
|
+
deletion.phase === "transcript" ? deletion.retainedUnknownPath : active.retainedTranscriptUnknownPath,
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1588
2031
|
async function publishCleanupPending(
|
|
1589
2032
|
scope: ManagedScope,
|
|
1590
2033
|
tombstone: string,
|
|
@@ -1846,7 +2289,7 @@ function planArtifactRootForMigration(sourceTranscript: string, operation: strin
|
|
|
1846
2289
|
dev: stat.dev,
|
|
1847
2290
|
ino: stat.ino,
|
|
1848
2291
|
size: process.platform === "win32" ? BigInt(root.size) : stat.size,
|
|
1849
|
-
mtimeNs: stat.mtimeNs,
|
|
2292
|
+
mtimeNs: process.platform === "win32" ? BigInt(root.mtimeNs) : stat.mtimeNs,
|
|
1850
2293
|
},
|
|
1851
2294
|
tree,
|
|
1852
2295
|
};
|
|
@@ -1869,47 +2312,194 @@ function sameDirectoryObject(leftPath: string, rightPath: string): boolean {
|
|
|
1869
2312
|
}
|
|
1870
2313
|
}
|
|
1871
2314
|
|
|
1872
|
-
function
|
|
2315
|
+
export function matchesMigrationArtifactRoot(
|
|
2316
|
+
pathname: string,
|
|
2317
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint },
|
|
2318
|
+
expectedTree: NativeDirectoryTreeSnapshot,
|
|
2319
|
+
platform: NodeJS.Platform = process.platform,
|
|
2320
|
+
): boolean {
|
|
1873
2321
|
try {
|
|
1874
2322
|
const stat = fs.lstatSync(pathname, { bigint: true });
|
|
2323
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== identity.dev || stat.ino !== identity.ino)
|
|
2324
|
+
return false;
|
|
2325
|
+
const observed = native.snapshotDirectoryTree(pathname);
|
|
2326
|
+
const expectedRoot = expectedTree.entries.find(entry => entry.relativePath === "" && entry.kind === "directory");
|
|
2327
|
+
const observedRoot = observed.snapshot?.entries.find(
|
|
2328
|
+
entry => entry.relativePath === "" && entry.kind === "directory",
|
|
2329
|
+
);
|
|
2330
|
+
if (
|
|
2331
|
+
!observed.ok ||
|
|
2332
|
+
!observed.snapshot ||
|
|
2333
|
+
!expectedRoot ||
|
|
2334
|
+
!observedRoot ||
|
|
2335
|
+
(platform === "win32" &&
|
|
2336
|
+
(observedRoot.dev !== identity.dev.toString() ||
|
|
2337
|
+
observedRoot.ino !== identity.ino.toString() ||
|
|
2338
|
+
BigInt(observedRoot.size) !== identity.size ||
|
|
2339
|
+
BigInt(observedRoot.mtimeNs) !== identity.mtimeNs)) ||
|
|
2340
|
+
(platform !== "win32" && (stat.size !== identity.size || stat.mtimeNs !== identity.mtimeNs))
|
|
2341
|
+
)
|
|
2342
|
+
return false;
|
|
2343
|
+
return sameArtifactTree(expectedTree, observed.snapshot, { platform, phase: "migration" });
|
|
2344
|
+
} catch {
|
|
2345
|
+
return false;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
type ArtifactTreeComparisonPolicy = {
|
|
2350
|
+
platform?: NodeJS.Platform;
|
|
2351
|
+
phase?: "migration" | "strict";
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
function sameArtifactTree(
|
|
2355
|
+
left: NativeDirectoryTreeSnapshot,
|
|
2356
|
+
right: NativeDirectoryTreeSnapshot,
|
|
2357
|
+
policy: ArtifactTreeComparisonPolicy = {},
|
|
2358
|
+
): boolean {
|
|
2359
|
+
const { platform = process.platform, phase = "strict" } = policy;
|
|
2360
|
+
const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string => {
|
|
2361
|
+
// Detaching the root directory changes its ctime without changing the preserved artifact tree;
|
|
2362
|
+
// root identity and mtime are checked above. pi-iso also accepts Windows plain directories by
|
|
2363
|
+
// stable file id and kind only because NTFS can lazily update their metadata while enumeration
|
|
2364
|
+
// trails an open handle. All other platforms and nested directories compare every captured field.
|
|
2365
|
+
if (phase === "migration" && entry.kind === "directory" && (entry.relativePath === "" || platform === "win32"))
|
|
2366
|
+
return JSON.stringify([entry.relativePath, entry.kind, entry.dev, entry.ino]);
|
|
2367
|
+
return JSON.stringify([
|
|
2368
|
+
entry.relativePath,
|
|
2369
|
+
entry.kind,
|
|
2370
|
+
entry.dev,
|
|
2371
|
+
entry.ino,
|
|
2372
|
+
entry.size,
|
|
2373
|
+
entry.mtimeNs,
|
|
2374
|
+
entry.ctimeNs,
|
|
2375
|
+
entry.sha256,
|
|
2376
|
+
]);
|
|
2377
|
+
};
|
|
2378
|
+
const leftEntries = left.entries.map(entryKey).sort();
|
|
2379
|
+
const rightEntries = right.entries.map(entryKey).sort();
|
|
2380
|
+
return (
|
|
2381
|
+
left.rootDev === right.rootDev &&
|
|
2382
|
+
left.rootIno === right.rootIno &&
|
|
2383
|
+
leftEntries.length === rightEntries.length &&
|
|
2384
|
+
leftEntries.every((entry, index) => entry === rightEntries[index])
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function matchesDetachedArtifactRoot(pathname: string, plan: DetachedArtifactRoot): boolean {
|
|
2389
|
+
return (
|
|
2390
|
+
matchesMigrationArtifactRoot(pathname, plan.identity, plan.tree) &&
|
|
2391
|
+
sameDirectoryObject(path.dirname(pathname), path.dirname(plan.detachedPath))
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
type SourceArtifactCleanup = {
|
|
2396
|
+
state: "cleanup_pending";
|
|
2397
|
+
role: "exchange_placeholder";
|
|
2398
|
+
retainedPath: string;
|
|
2399
|
+
identity: DetachedArtifactRoot["identity"];
|
|
2400
|
+
tree: NativeDirectoryTreeSnapshot;
|
|
2401
|
+
};
|
|
2402
|
+
|
|
2403
|
+
function cleanupAuthorityMatches(cleanup: SourceArtifactCleanup, parent: string): boolean {
|
|
2404
|
+
try {
|
|
2405
|
+
const stat = fs.lstatSync(cleanup.retainedPath, { bigint: true });
|
|
2406
|
+
if (
|
|
2407
|
+
path.dirname(cleanup.retainedPath) !== parent ||
|
|
2408
|
+
!stat.isDirectory() ||
|
|
2409
|
+
stat.isSymbolicLink() ||
|
|
2410
|
+
stat.dev !== cleanup.identity.dev ||
|
|
2411
|
+
stat.ino !== cleanup.identity.ino ||
|
|
2412
|
+
stat.size !== cleanup.identity.size ||
|
|
2413
|
+
stat.mtimeNs !== cleanup.identity.mtimeNs
|
|
2414
|
+
)
|
|
2415
|
+
return false;
|
|
2416
|
+
const snapshot = native.snapshotDirectoryTree(cleanup.retainedPath);
|
|
1875
2417
|
return (
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
sameDirectoryObject(path.dirname(pathname), path.dirname(plan.detachedPath))
|
|
2418
|
+
!!snapshot.ok &&
|
|
2419
|
+
!!snapshot.snapshot &&
|
|
2420
|
+
sameArtifactTree(snapshot.snapshot, cleanup.tree) &&
|
|
2421
|
+
cleanup.tree.entries.length === 1 &&
|
|
2422
|
+
cleanup.tree.entries[0]?.relativePath === "" &&
|
|
2423
|
+
cleanup.tree.entries[0]?.kind === "directory"
|
|
1883
2424
|
);
|
|
1884
2425
|
} catch {
|
|
1885
2426
|
return false;
|
|
1886
2427
|
}
|
|
1887
2428
|
}
|
|
1888
2429
|
|
|
1889
|
-
function detachArtifactRootForMigration(
|
|
2430
|
+
function detachArtifactRootForMigration(
|
|
2431
|
+
plan: DetachedArtifactRoot,
|
|
2432
|
+
):
|
|
2433
|
+
| { detached: DetachedArtifactRoot; detachOutcome: "clean" }
|
|
2434
|
+
| { detached: DetachedArtifactRoot; detachOutcome: "cleanup_pending"; cleanup: SourceArtifactCleanup } {
|
|
1890
2435
|
const result = native.exactUnlink(plan.originalPath, {
|
|
1891
2436
|
...plan.identity,
|
|
1892
2437
|
directory: true,
|
|
1893
2438
|
detachOnly: true,
|
|
1894
2439
|
quarantineName: path.basename(plan.detachedPath),
|
|
1895
2440
|
});
|
|
1896
|
-
|
|
2441
|
+
const cleanSuccess =
|
|
2442
|
+
result.ok && !result.retainedSuccessorPath && !result.retainedPlaceholderPath && !result.retainedUnknownPath;
|
|
2443
|
+
const cleanupPending =
|
|
2444
|
+
!result.ok &&
|
|
2445
|
+
result.code === "cleanup_pending" &&
|
|
2446
|
+
!!result.detachedPath &&
|
|
2447
|
+
!!result.retainedPlaceholderPath &&
|
|
2448
|
+
!result.retainedSuccessorPath &&
|
|
2449
|
+
!result.retainedUnknownPath;
|
|
2450
|
+
if (
|
|
2451
|
+
(!cleanSuccess && !cleanupPending) ||
|
|
2452
|
+
!result.detachedPath ||
|
|
2453
|
+
!matchesDetachedArtifactRoot(result.detachedPath, plan)
|
|
2454
|
+
)
|
|
1897
2455
|
throw new Error("durability_failed");
|
|
2456
|
+
|
|
1898
2457
|
const detachedPath =
|
|
1899
2458
|
process.platform === "win32" ? fs.realpathSync.native(result.detachedPath) : result.detachedPath;
|
|
1900
2459
|
if (!matchesDetachedArtifactRoot(detachedPath, plan)) throw new Error("durability_failed");
|
|
1901
|
-
|
|
2460
|
+
const detached = { ...plan, detachedPath };
|
|
2461
|
+
if (!cleanupPending) return { detached, detachOutcome: "clean" };
|
|
2462
|
+
const placeholder = result.retainedPlaceholderPath!;
|
|
2463
|
+
const stat = fs.lstatSync(placeholder, { bigint: true });
|
|
2464
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || path.dirname(placeholder) !== path.dirname(plan.originalPath))
|
|
2465
|
+
throw new Error("durability_failed");
|
|
2466
|
+
const snapshot = native.snapshotDirectoryTree(placeholder);
|
|
2467
|
+
const cleanup: SourceArtifactCleanup = {
|
|
2468
|
+
state: "cleanup_pending",
|
|
2469
|
+
role: "exchange_placeholder",
|
|
2470
|
+
retainedPath: placeholder,
|
|
2471
|
+
identity: { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeNs: stat.mtimeNs },
|
|
2472
|
+
tree: snapshot.snapshot!,
|
|
2473
|
+
};
|
|
2474
|
+
if (!snapshot.ok || !snapshot.snapshot || !cleanupAuthorityMatches(cleanup, path.dirname(plan.originalPath)))
|
|
2475
|
+
throw new Error("durability_failed");
|
|
2476
|
+
return { detached, detachOutcome: "cleanup_pending", cleanup };
|
|
1902
2477
|
}
|
|
1903
2478
|
|
|
1904
|
-
function restorePreparedArtifactRoot(
|
|
1905
|
-
|
|
2479
|
+
export function restorePreparedArtifactRoot(
|
|
2480
|
+
scope: ManagedScope,
|
|
2481
|
+
source: ManagedCandidate,
|
|
2482
|
+
lock?: ManagedStorageLock,
|
|
2483
|
+
): void {
|
|
2484
|
+
const preparedReceipt = receiptPathFor(scope, source, "prepared");
|
|
2485
|
+
const detachedReceipt = receiptPathFor(scope, source, "detached");
|
|
2486
|
+
const receipt = fs.existsSync(detachedReceipt) ? detachedReceipt : preparedReceipt;
|
|
1906
2487
|
let record: {
|
|
1907
2488
|
sourceArtifactQuarantine?: {
|
|
1908
2489
|
path?: unknown;
|
|
1909
2490
|
detachedPath?: unknown;
|
|
1910
2491
|
identity?: Record<string, unknown>;
|
|
1911
2492
|
tree?: unknown;
|
|
2493
|
+
role?: unknown;
|
|
2494
|
+
};
|
|
2495
|
+
sourceArtifactCleanup?: {
|
|
2496
|
+
state?: unknown;
|
|
2497
|
+
role?: unknown;
|
|
2498
|
+
retainedPath?: unknown;
|
|
2499
|
+
identity?: Record<string, unknown>;
|
|
2500
|
+
tree?: unknown;
|
|
1912
2501
|
};
|
|
2502
|
+
detachOutcome?: unknown;
|
|
1913
2503
|
};
|
|
1914
2504
|
try {
|
|
1915
2505
|
record = JSON.parse(captureManagedFileNoFollow(receipt).bytes.toString("utf8")) as typeof record;
|
|
@@ -1933,39 +2523,105 @@ function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandida
|
|
|
1933
2523
|
typeof identity.mtimeNs !== "string"
|
|
1934
2524
|
)
|
|
1935
2525
|
throw new Error("durability_failed");
|
|
2526
|
+
const artifactIdentity = {
|
|
2527
|
+
dev: BigInt(identity.dev),
|
|
2528
|
+
ino: BigInt(identity.ino),
|
|
2529
|
+
size: BigInt(identity.size),
|
|
2530
|
+
mtimeNs: BigInt(identity.mtimeNs),
|
|
2531
|
+
};
|
|
1936
2532
|
const expectedTree = artifactTreeSnapshot(quarantine.tree)!;
|
|
1937
2533
|
const assertPreparedTree = (pathname: string): void => {
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2534
|
+
if (
|
|
2535
|
+
!matchesMigrationArtifactRoot(
|
|
2536
|
+
pathname,
|
|
2537
|
+
{
|
|
2538
|
+
...artifactIdentity,
|
|
2539
|
+
},
|
|
2540
|
+
expectedTree,
|
|
2541
|
+
)
|
|
2542
|
+
)
|
|
1941
2543
|
throw new Error("durability_failed");
|
|
1942
2544
|
};
|
|
2545
|
+
if (receipt === detachedReceipt) {
|
|
2546
|
+
if (quarantine.role !== "detached_artifact_root") throw new Error("durability_failed");
|
|
2547
|
+
if (record.detachOutcome === "clean") {
|
|
2548
|
+
if (record.sourceArtifactCleanup !== undefined) throw new Error("durability_failed");
|
|
2549
|
+
const originalExists = fs.existsSync(quarantine.path);
|
|
2550
|
+
const detachedExists = fs.existsSync(quarantine.detachedPath);
|
|
2551
|
+
if (originalExists) {
|
|
2552
|
+
if (detachedExists) throw new Error("durability_failed");
|
|
2553
|
+
assertPreparedTree(quarantine.path);
|
|
2554
|
+
if (lock) {
|
|
2555
|
+
lock.assertOwned();
|
|
2556
|
+
fs.unlinkSync(receipt);
|
|
2557
|
+
fsyncManagedParent(receipt);
|
|
2558
|
+
}
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
} else if (record.detachOutcome === "cleanup_pending") {
|
|
2562
|
+
const cleanup = record.sourceArtifactCleanup;
|
|
2563
|
+
if (!cleanup) throw new Error("durability_failed");
|
|
2564
|
+
const cleanupIdentity = cleanup.identity;
|
|
2565
|
+
const cleanupTree = artifactTreeSnapshot(cleanup.tree);
|
|
2566
|
+
if (
|
|
2567
|
+
cleanup.state !== "cleanup_pending" ||
|
|
2568
|
+
cleanup.role !== "exchange_placeholder" ||
|
|
2569
|
+
typeof cleanup.retainedPath !== "string" ||
|
|
2570
|
+
!cleanupIdentity ||
|
|
2571
|
+
typeof cleanupIdentity.dev !== "string" ||
|
|
2572
|
+
typeof cleanupIdentity.ino !== "string" ||
|
|
2573
|
+
typeof cleanupIdentity.size !== "string" ||
|
|
2574
|
+
typeof cleanupIdentity.mtimeNs !== "string" ||
|
|
2575
|
+
!cleanupTree ||
|
|
2576
|
+
!cleanupAuthorityMatches(
|
|
2577
|
+
{
|
|
2578
|
+
state: "cleanup_pending",
|
|
2579
|
+
role: "exchange_placeholder",
|
|
2580
|
+
retainedPath: cleanup.retainedPath,
|
|
2581
|
+
identity: {
|
|
2582
|
+
dev: BigInt(cleanupIdentity.dev),
|
|
2583
|
+
ino: BigInt(cleanupIdentity.ino),
|
|
2584
|
+
size: BigInt(cleanupIdentity.size),
|
|
2585
|
+
mtimeNs: BigInt(cleanupIdentity.mtimeNs),
|
|
2586
|
+
},
|
|
2587
|
+
tree: cleanupTree,
|
|
2588
|
+
},
|
|
2589
|
+
path.dirname(source.path),
|
|
2590
|
+
)
|
|
2591
|
+
)
|
|
2592
|
+
throw new Error("durability_failed");
|
|
2593
|
+
} else {
|
|
2594
|
+
throw new Error("durability_failed");
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
1943
2598
|
if (fs.existsSync(quarantine.path)) {
|
|
1944
|
-
const existing = fs.lstatSync(quarantine.path, { bigint: true });
|
|
1945
2599
|
if (
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2600
|
+
matchesMigrationArtifactRoot(
|
|
2601
|
+
quarantine.path,
|
|
2602
|
+
{
|
|
2603
|
+
...artifactIdentity,
|
|
2604
|
+
},
|
|
2605
|
+
expectedTree,
|
|
2606
|
+
)
|
|
2607
|
+
) {
|
|
2608
|
+
assertPreparedTree(quarantine.path);
|
|
2609
|
+
}
|
|
2610
|
+
// A changed source pathname is independent retained authority. Do not replace
|
|
2611
|
+
// it with the detached original; retain both roots for recovery.
|
|
1955
2612
|
return;
|
|
1956
2613
|
}
|
|
1957
2614
|
assertPreparedTree(quarantine.detachedPath);
|
|
1958
2615
|
const result = native.exactRestore(quarantine.detachedPath, quarantine.path, {
|
|
1959
|
-
|
|
1960
|
-
ino: BigInt(identity.ino),
|
|
1961
|
-
size: BigInt(identity.size),
|
|
1962
|
-
mtimeNs: BigInt(identity.mtimeNs),
|
|
2616
|
+
...artifactIdentity,
|
|
1963
2617
|
directory: true,
|
|
1964
2618
|
});
|
|
1965
2619
|
if (!result.ok) throw new Error("durability_failed");
|
|
1966
2620
|
}
|
|
1967
2621
|
|
|
1968
|
-
function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot): void {
|
|
2622
|
+
function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot, cleanup?: SourceArtifactCleanup): void {
|
|
2623
|
+
if (cleanup && !cleanupAuthorityMatches(cleanup, path.dirname(detached.originalPath)))
|
|
2624
|
+
throw new Error("durability_failed");
|
|
1969
2625
|
const result = native.exactRestore(detached.detachedPath, detached.originalPath, {
|
|
1970
2626
|
...detached.identity,
|
|
1971
2627
|
directory: true,
|
|
@@ -1988,40 +2644,41 @@ async function copyArtifacts(
|
|
|
1988
2644
|
const sourceRoot = sourceRootOverride ?? sourceTranscript.slice(0, -6);
|
|
1989
2645
|
if (!manifestMatches(sourceTranscript, manifest, sourceRoot)) throw new Error("source_changed");
|
|
1990
2646
|
const destinationRoot = destinationTranscript.slice(0, -6);
|
|
1991
|
-
for (
|
|
2647
|
+
for (let start = 0; start < manifest.length; start += MANAGED_ARTIFACT_COPY_BATCH_SIZE) {
|
|
2648
|
+
const batch = manifest.slice(start, start + MANAGED_ARTIFACT_COPY_BATCH_SIZE);
|
|
1992
2649
|
revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
|
|
1993
2650
|
lock.assertOwned();
|
|
1994
|
-
const
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
if (entry.
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2651
|
+
for (const entry of batch) {
|
|
2652
|
+
const source = path.join(sourceRoot, entry.path);
|
|
2653
|
+
const destination = path.join(destinationRoot, entry.path);
|
|
2654
|
+
if (entry.kind === "directory") {
|
|
2655
|
+
if (entry.path === "") {
|
|
2656
|
+
ensureManagedDirectory(destinationRoot, root);
|
|
2657
|
+
} else {
|
|
2658
|
+
ensureManagedDirectory(destination, root);
|
|
2659
|
+
}
|
|
2660
|
+
continue;
|
|
2003
2661
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2662
|
+
const snapshot = captureManagedFileNoFollow(source);
|
|
2663
|
+
if (
|
|
2664
|
+
snapshot.bytes.byteLength !== entry.size ||
|
|
2665
|
+
createHash("sha256").update(snapshot.bytes).digest("hex") !== entry.sha256
|
|
2666
|
+
)
|
|
2667
|
+
throw new Error("source_changed");
|
|
2668
|
+
try {
|
|
2669
|
+
await copyManagedFileNoReplace(source, destination, snapshot, root);
|
|
2670
|
+
} catch (error) {
|
|
2671
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2672
|
+
}
|
|
2673
|
+
lock.assertOwned();
|
|
2674
|
+
const copied = captureManagedFileNoFollow(destination);
|
|
2675
|
+
if (
|
|
2676
|
+
copied.bytes.byteLength !== entry.size ||
|
|
2677
|
+
createHash("sha256").update(copied.bytes).digest("hex") !== entry.sha256
|
|
2678
|
+
)
|
|
2679
|
+
throw new Error("durability_failed");
|
|
2017
2680
|
}
|
|
2018
|
-
|
|
2019
|
-
const copied = captureManagedFileNoFollow(destination);
|
|
2020
|
-
if (
|
|
2021
|
-
copied.bytes.byteLength !== entry.size ||
|
|
2022
|
-
createHash("sha256").update(copied.bytes).digest("hex") !== entry.sha256
|
|
2023
|
-
)
|
|
2024
|
-
throw new Error("durability_failed");
|
|
2681
|
+
if (start + batch.length < manifest.length) await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
2025
2682
|
}
|
|
2026
2683
|
if (!manifestMatches(sourceTranscript, manifest, sourceRoot) || !manifestMatches(destinationTranscript, manifest))
|
|
2027
2684
|
throw new Error("durability_failed");
|
|
@@ -2030,7 +2687,7 @@ async function copyArtifacts(
|
|
|
2030
2687
|
function migrationReceipt(
|
|
2031
2688
|
scope: ManagedScope,
|
|
2032
2689
|
lock: ManagedStorageLock,
|
|
2033
|
-
state: "prepared" | "published" | "committed",
|
|
2690
|
+
state: "prepared" | "artifact_detached" | "published" | "committed",
|
|
2034
2691
|
source: ManagedCandidate,
|
|
2035
2692
|
destination: ManagedCandidate | { path: string; sessionId: string; cwd: string },
|
|
2036
2693
|
manifest: readonly ArtifactManifestEntry[],
|
|
@@ -2039,7 +2696,10 @@ function migrationReceipt(
|
|
|
2039
2696
|
detachedPath: string;
|
|
2040
2697
|
identity: DetachedArtifactRoot["identity"];
|
|
2041
2698
|
tree: NativeDirectoryTreeSnapshot;
|
|
2699
|
+
role?: "detached_artifact_root";
|
|
2042
2700
|
},
|
|
2701
|
+
sourceArtifactCleanup?: SourceArtifactCleanup,
|
|
2702
|
+
detachOutcome?: "clean" | "cleanup_pending",
|
|
2043
2703
|
): Uint8Array {
|
|
2044
2704
|
lock.assertOwned();
|
|
2045
2705
|
const destinationRecord =
|
|
@@ -2057,16 +2717,27 @@ function migrationReceipt(
|
|
|
2057
2717
|
header: { id: destination.sessionId, cwd: destination.cwd },
|
|
2058
2718
|
};
|
|
2059
2719
|
return new TextEncoder().encode(
|
|
2060
|
-
`${JSON.stringify({ schemaVersion: 2, state, policy: "copy-retain", attemptId: lock.attemptId, scope: scopeDigest(scope.platform, scope.canonicalCwd), source: { path: source.path, sessionId: source.sessionId, header: { id: source.sessionId, cwd: source.cwd }, identity: source.identity, sha256: source.identity.sha256 }, destination: destinationRecord, artifactManifest: manifest, ...(sourceArtifactQuarantine ? { sourceArtifactQuarantine } : {}) }, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value))}\n`,
|
|
2720
|
+
`${JSON.stringify({ schemaVersion: 2, state, policy: "copy-retain", attemptId: lock.attemptId, scope: scopeDigest(scope.platform, scope.canonicalCwd), source: { path: source.path, sessionId: source.sessionId, header: { id: source.sessionId, cwd: source.cwd }, identity: source.identity, sha256: source.identity.sha256 }, destination: destinationRecord, artifactManifest: manifest, ...(sourceArtifactQuarantine ? { sourceArtifactQuarantine } : {}), ...(sourceArtifactCleanup ? { sourceArtifactCleanup } : {}), ...(detachOutcome ? { detachOutcome } : {}) }, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value))}\n`,
|
|
2061
2721
|
);
|
|
2062
2722
|
}
|
|
2063
2723
|
|
|
2724
|
+
function detachedReceiptMatches(receipt: string, expected: Uint8Array): boolean {
|
|
2725
|
+
try {
|
|
2726
|
+
return captureManagedFileNoFollow(receipt).bytes.equals(expected);
|
|
2727
|
+
} catch {
|
|
2728
|
+
return false;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2064
2732
|
async function removeStagedReceipts(scope: ManagedScope, candidate: ManagedCandidate): Promise<void> {
|
|
2065
|
-
for (const state of ["prepared", "published"] as const) {
|
|
2733
|
+
for (const state of ["prepared", "detached", "published"] as const) {
|
|
2066
2734
|
const pathname = receiptPathFor(scope, candidate, state);
|
|
2067
2735
|
try {
|
|
2068
2736
|
const stat = fs.lstatSync(pathname);
|
|
2069
|
-
if (stat.isFile() || stat.isSymbolicLink())
|
|
2737
|
+
if (stat.isFile() || stat.isSymbolicLink()) {
|
|
2738
|
+
await fs.promises.unlink(pathname);
|
|
2739
|
+
fsyncManagedParent(pathname);
|
|
2740
|
+
}
|
|
2070
2741
|
} catch (error) {
|
|
2071
2742
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
2072
2743
|
}
|
|
@@ -2145,8 +2816,7 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2145
2816
|
if (!observedPending) continue;
|
|
2146
2817
|
if (
|
|
2147
2818
|
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2148
|
-
|
|
2149
|
-
!fs.existsSync(observedPending.plannedTranscriptPath)
|
|
2819
|
+
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2150
2820
|
) {
|
|
2151
2821
|
fsyncManagedParent(target.path);
|
|
2152
2822
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -2185,6 +2855,12 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2185
2855
|
? pending.plannedTranscriptPath
|
|
2186
2856
|
: undefined) ??
|
|
2187
2857
|
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
2858
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2859
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2860
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2861
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2862
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2863
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2188
2864
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2189
2865
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2190
2866
|
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
@@ -2201,6 +2877,14 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2201
2877
|
transcriptIdentity: target.identity,
|
|
2202
2878
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2203
2879
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2880
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
2881
|
+
|
|
2882
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2883
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2884
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2885
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2886
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2887
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2204
2888
|
artifactsRemoved: true,
|
|
2205
2889
|
});
|
|
2206
2890
|
}
|
|
@@ -2221,10 +2905,34 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2221
2905
|
deletion.phase === "transcript"
|
|
2222
2906
|
? deletion.detachedTranscriptPath
|
|
2223
2907
|
: active.detachedTranscriptPath,
|
|
2908
|
+
retainedArtifactsSuccessorPath:
|
|
2909
|
+
deletion.phase === "artifacts"
|
|
2910
|
+
? deletion.retainedSuccessorPath
|
|
2911
|
+
: active.retainedArtifactsSuccessorPath,
|
|
2912
|
+
retainedArtifactsPlaceholderPath:
|
|
2913
|
+
deletion.phase === "artifacts"
|
|
2914
|
+
? deletion.retainedPlaceholderPath
|
|
2915
|
+
: active.retainedArtifactsPlaceholderPath,
|
|
2916
|
+
retainedArtifactsUnknownPath:
|
|
2917
|
+
deletion.phase === "artifacts"
|
|
2918
|
+
? deletion.retainedUnknownPath
|
|
2919
|
+
: active.retainedArtifactsUnknownPath,
|
|
2920
|
+
retainedTranscriptSuccessorPath:
|
|
2921
|
+
deletion.phase === "transcript"
|
|
2922
|
+
? deletion.retainedSuccessorPath
|
|
2923
|
+
: active.retainedTranscriptSuccessorPath,
|
|
2924
|
+
retainedTranscriptPlaceholderPath:
|
|
2925
|
+
deletion.phase === "transcript"
|
|
2926
|
+
? deletion.retainedPlaceholderPath
|
|
2927
|
+
: active.retainedTranscriptPlaceholderPath,
|
|
2928
|
+
retainedTranscriptUnknownPath:
|
|
2929
|
+
deletion.phase === "transcript"
|
|
2930
|
+
? deletion.retainedUnknownPath
|
|
2931
|
+
: active.retainedTranscriptUnknownPath,
|
|
2224
2932
|
},
|
|
2225
2933
|
lock,
|
|
2226
2934
|
);
|
|
2227
|
-
|
|
2935
|
+
continue;
|
|
2228
2936
|
}
|
|
2229
2937
|
fsyncManagedParent(target.path);
|
|
2230
2938
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -2257,11 +2965,24 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
2257
2965
|
await reconcileManagedTombstones(scope);
|
|
2258
2966
|
return { kind: "resolved", scope };
|
|
2259
2967
|
} catch (error) {
|
|
2260
|
-
|
|
2968
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
2969
|
+
const message =
|
|
2970
|
+
publication?.classification ??
|
|
2971
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
2972
|
+
const code =
|
|
2973
|
+
message === "atomic_unavailable" ||
|
|
2974
|
+
message === "invalid_request" ||
|
|
2975
|
+
message === "durability_failed" ||
|
|
2976
|
+
message === "durability_not_provable"
|
|
2977
|
+
? message
|
|
2978
|
+
: "binding_invalid";
|
|
2261
2979
|
return {
|
|
2262
2980
|
kind: "error",
|
|
2263
|
-
code
|
|
2264
|
-
message
|
|
2981
|
+
code,
|
|
2982
|
+
message,
|
|
2983
|
+
...(publication
|
|
2984
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
2985
|
+
: { cause: { classification: code } }),
|
|
2265
2986
|
};
|
|
2266
2987
|
}
|
|
2267
2988
|
}
|
|
@@ -2300,7 +3021,8 @@ export async function openManagedCandidateForWrite(
|
|
|
2300
3021
|
if (prepared.kind === "error")
|
|
2301
3022
|
return {
|
|
2302
3023
|
kind: "error",
|
|
2303
|
-
code: prepared.code
|
|
3024
|
+
code: expectedFailure(new Error(prepared.code)),
|
|
3025
|
+
|
|
2304
3026
|
message: prepared.message,
|
|
2305
3027
|
};
|
|
2306
3028
|
current = revalidatePickerConsent(scope, candidate, expectedIdentity);
|
|
@@ -2319,11 +3041,15 @@ export async function openManagedCandidateForWrite(
|
|
|
2319
3041
|
const internal = managedInternalDirectory(scope);
|
|
2320
3042
|
let lock: ManagedStorageLock | undefined;
|
|
2321
3043
|
let detachedArtifacts: DetachedArtifactRoot | undefined;
|
|
3044
|
+
let sourceArtifactCleanup: SourceArtifactCleanup | undefined;
|
|
3045
|
+
|
|
2322
3046
|
try {
|
|
2323
3047
|
revalidatePickerConsent(scope, current, expectedIdentity);
|
|
2324
3048
|
lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation, scopeRoot(scope));
|
|
2325
3049
|
const heldLock = lock;
|
|
3050
|
+
|
|
2326
3051
|
const afterLock = revalidatePickerConsent(scope, current, expectedIdentity);
|
|
3052
|
+
|
|
2327
3053
|
const listing = listManagedCandidates(scope);
|
|
2328
3054
|
if (listing.kind === "error") return { kind: "error", code: "binding_invalid", message: listing.message };
|
|
2329
3055
|
const destination = path.join(scope.directoryPath, path.basename(afterLock.path));
|
|
@@ -2335,11 +3061,22 @@ export async function openManagedCandidateForWrite(
|
|
|
2335
3061
|
code: "destination_conflict",
|
|
2336
3062
|
message: "A distinct v2 transcript already owns this session id.",
|
|
2337
3063
|
};
|
|
3064
|
+
|
|
2338
3065
|
scopeRoot(scope);
|
|
2339
3066
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2340
|
-
restorePreparedArtifactRoot(scope, afterLock);
|
|
3067
|
+
restorePreparedArtifactRoot(scope, afterLock, heldLock);
|
|
2341
3068
|
scopeRoot(scope);
|
|
2342
|
-
|
|
3069
|
+
|
|
3070
|
+
let sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
|
|
3071
|
+
if (
|
|
3072
|
+
sourceSnapshot.identity.dev !== afterLock.identity.dev ||
|
|
3073
|
+
sourceSnapshot.identity.ino !== afterLock.identity.ino ||
|
|
3074
|
+
sourceSnapshot.identity.size !== afterLock.identity.size ||
|
|
3075
|
+
sourceSnapshot.identity.mtimeNs !== afterLock.identity.mtimeNs ||
|
|
3076
|
+
sourceSnapshot.identity.sha256 !== afterLock.identity.sha256 ||
|
|
3077
|
+
sourceSnapshot.bytes.byteLength !== afterLock.identity.size
|
|
3078
|
+
)
|
|
3079
|
+
throw new Error("source_changed");
|
|
2343
3080
|
let manifest: readonly ArtifactManifestEntry[] = [];
|
|
2344
3081
|
const artifactPlan = planArtifactRootForMigration(afterLock.path, operation);
|
|
2345
3082
|
const intendedDestination = { path: destination, sessionId: afterLock.sessionId, cwd: afterLock.cwd };
|
|
@@ -2361,6 +3098,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2361
3098
|
message: "The migration destination already exists without validated ownership.",
|
|
2362
3099
|
};
|
|
2363
3100
|
}
|
|
3101
|
+
|
|
2364
3102
|
const preparedReceipt = receiptPathFor(scope, afterLock, "prepared");
|
|
2365
3103
|
try {
|
|
2366
3104
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
@@ -2389,14 +3127,53 @@ export async function openManagedCandidateForWrite(
|
|
|
2389
3127
|
} catch (error) {
|
|
2390
3128
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2391
3129
|
}
|
|
3130
|
+
|
|
3131
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2392
3132
|
lock.assertOwned();
|
|
2393
3133
|
if (!preparedReceiptMatches(preparedReceipt, scope, afterLock, intendedDestination, artifactPlan))
|
|
2394
3134
|
throw new Error("durability_failed");
|
|
2395
3135
|
if (artifactPlan && fs.existsSync(artifactPlan.detachedPath)) throw new Error("destination_conflict");
|
|
3136
|
+
|
|
2396
3137
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3138
|
+
|
|
2397
3139
|
scopeRoot(scope);
|
|
2398
|
-
|
|
3140
|
+
|
|
3141
|
+
if (artifactPlan) {
|
|
3142
|
+
const detached = detachArtifactRootForMigration(artifactPlan);
|
|
3143
|
+
detachedArtifacts = detached.detached;
|
|
3144
|
+
sourceArtifactCleanup = detached.detachOutcome === "cleanup_pending" ? detached.cleanup : undefined;
|
|
3145
|
+
const detachedReceipt = receiptPathFor(scope, afterLock, "detached");
|
|
3146
|
+
const detachedRecord = migrationReceipt(
|
|
3147
|
+
scope,
|
|
3148
|
+
lock,
|
|
3149
|
+
"artifact_detached",
|
|
3150
|
+
afterLock,
|
|
3151
|
+
intendedDestination,
|
|
3152
|
+
[],
|
|
3153
|
+
{
|
|
3154
|
+
path: detachedArtifacts.originalPath,
|
|
3155
|
+
detachedPath: detachedArtifacts.detachedPath,
|
|
3156
|
+
identity: detachedArtifacts.identity,
|
|
3157
|
+
tree: detachedArtifacts.tree,
|
|
3158
|
+
role: "detached_artifact_root",
|
|
3159
|
+
},
|
|
3160
|
+
sourceArtifactCleanup,
|
|
3161
|
+
detached.detachOutcome,
|
|
3162
|
+
);
|
|
3163
|
+
try {
|
|
3164
|
+
await publishManagedFileNoReplace(
|
|
3165
|
+
detachedReceipt,
|
|
3166
|
+
detachedRecord,
|
|
3167
|
+
assertPublicationConsent,
|
|
3168
|
+
scopeRoot(scope),
|
|
3169
|
+
);
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
3172
|
+
}
|
|
3173
|
+
if (!detachedReceiptMatches(detachedReceipt, detachedRecord)) throw new Error("durability_failed");
|
|
3174
|
+
}
|
|
2399
3175
|
manifest = detachedArtifacts ? artifactManifestFromSnapshot(detachedArtifacts.tree) : [];
|
|
3176
|
+
|
|
2400
3177
|
await copyArtifacts(
|
|
2401
3178
|
scope,
|
|
2402
3179
|
afterLock.path,
|
|
@@ -2407,6 +3184,22 @@ export async function openManagedCandidateForWrite(
|
|
|
2407
3184
|
expectedIdentity,
|
|
2408
3185
|
detachedArtifacts?.detachedPath,
|
|
2409
3186
|
);
|
|
3187
|
+
|
|
3188
|
+
if (!existing) {
|
|
3189
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3190
|
+
lock.assertOwned();
|
|
3191
|
+
sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
|
|
3192
|
+
if (
|
|
3193
|
+
sourceSnapshot.identity.dev !== afterLock.identity.dev ||
|
|
3194
|
+
sourceSnapshot.identity.ino !== afterLock.identity.ino ||
|
|
3195
|
+
sourceSnapshot.identity.size !== afterLock.identity.size ||
|
|
3196
|
+
sourceSnapshot.identity.mtimeNs !== afterLock.identity.mtimeNs ||
|
|
3197
|
+
sourceSnapshot.identity.sha256 !== afterLock.identity.sha256 ||
|
|
3198
|
+
sourceSnapshot.bytes.byteLength !== afterLock.identity.size
|
|
3199
|
+
)
|
|
3200
|
+
throw new Error("source_changed");
|
|
3201
|
+
}
|
|
3202
|
+
|
|
2410
3203
|
if (!existing) {
|
|
2411
3204
|
try {
|
|
2412
3205
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
@@ -2416,6 +3209,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2416
3209
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2417
3210
|
}
|
|
2418
3211
|
}
|
|
3212
|
+
|
|
2419
3213
|
// Artifact files, directories, and the transcript must be durable before a receipt can grant shadow authority.
|
|
2420
3214
|
scopeRoot(scope);
|
|
2421
3215
|
if (manifest.length > 0) fsyncManagedArtifactTree(destination.slice(0, -6));
|
|
@@ -2429,12 +3223,14 @@ export async function openManagedCandidateForWrite(
|
|
|
2429
3223
|
!manifestMatches(destination, manifest)
|
|
2430
3224
|
)
|
|
2431
3225
|
throw new Error("durability_failed");
|
|
3226
|
+
|
|
2432
3227
|
if (detachedArtifacts) {
|
|
2433
3228
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2434
3229
|
scopeRoot(scope);
|
|
2435
|
-
restoreDetachedArtifactRoot(detachedArtifacts);
|
|
3230
|
+
restoreDetachedArtifactRoot(detachedArtifacts, sourceArtifactCleanup);
|
|
2436
3231
|
detachedArtifacts = undefined;
|
|
2437
3232
|
}
|
|
3233
|
+
|
|
2438
3234
|
const latest = validateCandidateForScope(scope, afterLock);
|
|
2439
3235
|
if (!latest || !sameCandidate(latest, afterLock) || !manifestMatches(afterLock.path, manifest))
|
|
2440
3236
|
return {
|
|
@@ -2442,6 +3238,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2442
3238
|
code: "source_changed",
|
|
2443
3239
|
message: "The legacy candidate or its artifacts changed before migration commit.",
|
|
2444
3240
|
};
|
|
3241
|
+
|
|
2445
3242
|
for (const state of ["published", "committed"] as const) {
|
|
2446
3243
|
const receipt = receiptPathFor(scope, afterLock, state);
|
|
2447
3244
|
try {
|
|
@@ -2449,7 +3246,24 @@ export async function openManagedCandidateForWrite(
|
|
|
2449
3246
|
lock.assertOwned();
|
|
2450
3247
|
await publishManagedFileNoReplace(
|
|
2451
3248
|
receipt,
|
|
2452
|
-
migrationReceipt(
|
|
3249
|
+
migrationReceipt(
|
|
3250
|
+
scope,
|
|
3251
|
+
lock,
|
|
3252
|
+
state,
|
|
3253
|
+
afterLock,
|
|
3254
|
+
migrated,
|
|
3255
|
+
manifest,
|
|
3256
|
+
artifactPlan
|
|
3257
|
+
? {
|
|
3258
|
+
path: artifactPlan.originalPath,
|
|
3259
|
+
detachedPath: artifactPlan.detachedPath,
|
|
3260
|
+
identity: artifactPlan.identity,
|
|
3261
|
+
tree: artifactPlan.tree,
|
|
3262
|
+
role: "detached_artifact_root",
|
|
3263
|
+
}
|
|
3264
|
+
: undefined,
|
|
3265
|
+
sourceArtifactCleanup,
|
|
3266
|
+
),
|
|
2453
3267
|
assertPublicationConsent,
|
|
2454
3268
|
scopeRoot(scope),
|
|
2455
3269
|
);
|
|
@@ -2457,6 +3271,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2457
3271
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2458
3272
|
}
|
|
2459
3273
|
}
|
|
3274
|
+
|
|
2460
3275
|
const receipt = receiptPathFor(scope, afterLock);
|
|
2461
3276
|
lock.assertOwned();
|
|
2462
3277
|
if (!receiptMatches(receipt, afterLock, migrated, scope))
|
|
@@ -2465,6 +3280,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2465
3280
|
code: "durability_failed",
|
|
2466
3281
|
message: "The migration receipt does not bind the copied v2 transcript and artifacts.",
|
|
2467
3282
|
};
|
|
3283
|
+
|
|
2468
3284
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2469
3285
|
scopeRoot(scope);
|
|
2470
3286
|
await removeStagedReceipts(scope, afterLock);
|
|
@@ -2476,7 +3292,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2476
3292
|
};
|
|
2477
3293
|
} catch (error) {
|
|
2478
3294
|
try {
|
|
2479
|
-
if (detachedArtifacts) restoreDetachedArtifactRoot(detachedArtifacts);
|
|
3295
|
+
if (detachedArtifacts) restoreDetachedArtifactRoot(detachedArtifacts, sourceArtifactCleanup);
|
|
2480
3296
|
} catch {
|
|
2481
3297
|
return {
|
|
2482
3298
|
kind: "error",
|
|
@@ -2485,7 +3301,16 @@ export async function openManagedCandidateForWrite(
|
|
|
2485
3301
|
};
|
|
2486
3302
|
}
|
|
2487
3303
|
const code = expectedFailure(error);
|
|
2488
|
-
return {
|
|
3304
|
+
return {
|
|
3305
|
+
kind: "error",
|
|
3306
|
+
code,
|
|
3307
|
+
message:
|
|
3308
|
+
code === "artifact_capacity_exceeded"
|
|
3309
|
+
? `Legacy session artifacts exceed the migration capacity (${MANAGED_ARTIFACT_MAX_FILES.toLocaleString()} files or ${MANAGED_ARTIFACT_MAX_TOTAL_BYTES / 1024 / 1024} MiB).`
|
|
3310
|
+
: error instanceof Error
|
|
3311
|
+
? error.message
|
|
3312
|
+
: "Managed migration failed.",
|
|
3313
|
+
};
|
|
2489
3314
|
} finally {
|
|
2490
3315
|
if (lock) await lock.release().catch(() => undefined);
|
|
2491
3316
|
}
|
|
@@ -2500,7 +3325,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
2500
3325
|
if (prepared.kind === "error")
|
|
2501
3326
|
return {
|
|
2502
3327
|
kind: "error",
|
|
2503
|
-
code: prepared.code
|
|
3328
|
+
code: expectedFailure(new Error(prepared.code)),
|
|
2504
3329
|
message: prepared.message,
|
|
2505
3330
|
};
|
|
2506
3331
|
const current = validateCandidateForScope(scope, candidate);
|
|
@@ -2560,8 +3385,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
2560
3385
|
if (!observedPending) continue;
|
|
2561
3386
|
if (
|
|
2562
3387
|
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2563
|
-
|
|
2564
|
-
!fs.existsSync(observedPending.plannedTranscriptPath)
|
|
3388
|
+
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2565
3389
|
) {
|
|
2566
3390
|
fsyncManagedParent(target.path);
|
|
2567
3391
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -2596,6 +3420,12 @@ export async function deleteManagedSessionCandidate(
|
|
|
2596
3420
|
active.detachedTranscriptPath ??
|
|
2597
3421
|
observedPending?.detachedTranscriptPath ??
|
|
2598
3422
|
(pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
|
|
3423
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3424
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3425
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3426
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3427
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3428
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2599
3429
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2600
3430
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2601
3431
|
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
@@ -2612,6 +3442,13 @@ export async function deleteManagedSessionCandidate(
|
|
|
2612
3442
|
transcriptIdentity: target.identity,
|
|
2613
3443
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2614
3444
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3445
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3446
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3447
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3448
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3449
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3450
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3451
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2615
3452
|
artifactsRemoved: true,
|
|
2616
3453
|
});
|
|
2617
3454
|
}
|
|
@@ -2627,23 +3464,51 @@ export async function deleteManagedSessionCandidate(
|
|
|
2627
3464
|
)
|
|
2628
3465
|
throw new Error("durability_failed");
|
|
2629
3466
|
const retry = nextCleanupReceipt(target, active);
|
|
2630
|
-
await publishCleanupPending(
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
3467
|
+
await publishCleanupPending(scope, tombstone, cleanupPendingEvidence(retry, active, deletion), lock);
|
|
3468
|
+
if (deletion.phase === "artifacts") {
|
|
3469
|
+
// A typed retained artifact root proves durable canonical absence; advance
|
|
3470
|
+
// to the transcript phase instead of stalling on the retained evidence.
|
|
3471
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
3472
|
+
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
3473
|
+
sessionsRoot: scope.sessionsRoot,
|
|
3474
|
+
transcriptPath: target.path,
|
|
3475
|
+
sessionId: target.sessionId,
|
|
3476
|
+
cwd: target.cwd,
|
|
3477
|
+
transcriptIdentity: target.identity,
|
|
3478
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3479
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3480
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3481
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3482
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3483
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3484
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3485
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3486
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
3487
|
+
artifactsRemoved: true,
|
|
3488
|
+
});
|
|
3489
|
+
if (deletion.kind === "cleanup_pending") {
|
|
3490
|
+
const followup = nextCleanupReceipt(target, active);
|
|
3491
|
+
await publishCleanupPending(
|
|
3492
|
+
scope,
|
|
3493
|
+
tombstone,
|
|
3494
|
+
cleanupPendingEvidence(followup, active, deletion),
|
|
3495
|
+
lock,
|
|
3496
|
+
);
|
|
3497
|
+
if (
|
|
3498
|
+
deletion.phase !== "transcript" ||
|
|
3499
|
+
deletion.detachedTranscriptPath !== active.plannedTranscriptPath
|
|
3500
|
+
)
|
|
3501
|
+
return {
|
|
3502
|
+
kind: "cleanup_pending",
|
|
3503
|
+
tombstonePath: tombstone,
|
|
3504
|
+
phase: deletion.phase,
|
|
3505
|
+
message:
|
|
3506
|
+
"Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
// A typed retained transcript quarantine is durable canonical-absence
|
|
3511
|
+
// evidence — never a terminal byte-deletion claim; complete the receipt.
|
|
2647
3512
|
}
|
|
2648
3513
|
fsyncManagedParent(target.path);
|
|
2649
3514
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|