@gajae-code/coding-agent 0.11.7 → 0.11.8
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 +55 -0
- package/dist/types/cli/args.d.ts +0 -2
- 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/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/repository-binding.d.ts +63 -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/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 +2 -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/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 +5 -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 +13 -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 +11 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-storage.d.ts +18 -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/notify-cli.ts +5 -38
- package/src/cli.ts +18 -0
- package/src/commands/sdk.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-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 +2 -2
- 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/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- 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/internal-urls/docs-index.generated.ts +6 -5
- 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 +22 -18
- 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 +41 -27
- 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 +6 -4
- package/src/prompts/tools/cron.md +3 -1
- 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/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +18 -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 +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +835 -110
- 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 +52 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +854 -84
- package/src/session/internal/managed-session-storage.ts +251 -199
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +310 -327
- package/src/session/session-storage.ts +128 -11
- 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 +198 -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,
|
|
@@ -24,10 +25,12 @@ import {
|
|
|
24
25
|
ensureManagedDirectory,
|
|
25
26
|
fsyncManagedArtifactTree,
|
|
26
27
|
type ManagedDirectoryRoot,
|
|
28
|
+
type ManagedFileSnapshot,
|
|
29
|
+
ManagedPublishError,
|
|
27
30
|
ManagedSessionDescendantStore,
|
|
28
31
|
type ManagedSessionSecurityPolicy,
|
|
29
32
|
type ManagedStorageLock,
|
|
30
|
-
|
|
33
|
+
prepareManagedDirectoryRoot,
|
|
31
34
|
publishManagedFileNoReplace,
|
|
32
35
|
publishManagedTombstone,
|
|
33
36
|
retainManagedDirectoryAuthority,
|
|
@@ -63,13 +66,12 @@ export interface ManagedCandidateWriteAuthority {
|
|
|
63
66
|
readonly retainedDirectory?: string;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof
|
|
69
|
+
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof prepareManagedDirectoryRoot>>();
|
|
67
70
|
const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
|
|
68
71
|
const managedDirectoryAuthorities = new WeakMap<ManagedScope, native.RecoveryFsRoot | undefined>();
|
|
69
72
|
const boundManagedWriteAuthorities = new WeakMap<ManagedScope, ManagedCandidateWriteAuthority>();
|
|
70
73
|
|
|
71
74
|
function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandidateWriteAuthority): void {
|
|
72
|
-
assertManagedDirectoryRoot(authority.rootAuthority);
|
|
73
75
|
if (
|
|
74
76
|
authority.retainedAuthority &&
|
|
75
77
|
authority.retainedDirectory !== undefined &&
|
|
@@ -79,11 +81,27 @@ function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandid
|
|
|
79
81
|
authority: authority.retainedAuthority,
|
|
80
82
|
authorityBaseDir: scope.directoryPath,
|
|
81
83
|
}).assertBound();
|
|
84
|
+
} else {
|
|
85
|
+
assertManagedDirectoryRoot(authority.rootAuthority);
|
|
82
86
|
}
|
|
83
87
|
managedRoots.set(scope, authority.rootAuthority);
|
|
84
88
|
boundManagedWriteAuthorities.set(scope, authority);
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
/** A prepared scope is a retained authority boundary, not a path that may be re-adopted. */
|
|
92
|
+
function assertRetainedManagedDirectoryIdentity(scope: ManagedScope): void {
|
|
93
|
+
const expected = managedDirectoryIdentities.get(scope);
|
|
94
|
+
if (!expected) return;
|
|
95
|
+
const current = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
96
|
+
if (
|
|
97
|
+
!current.isDirectory() ||
|
|
98
|
+
current.isSymbolicLink() ||
|
|
99
|
+
current.dev !== expected.dev ||
|
|
100
|
+
current.ino !== expected.ino
|
|
101
|
+
)
|
|
102
|
+
throw new Error("Managed session directory changed");
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
export function managedDirectoryAuthorityForScope(scope: ManagedScope): native.RecoveryFsRoot | undefined {
|
|
88
106
|
if (!managedDirectoryAuthorities.has(scope)) throw new Error("Managed session directory authority was not prepared");
|
|
89
107
|
return managedDirectoryAuthorities.get(scope);
|
|
@@ -97,21 +115,24 @@ export function managedDirectoryIdentityForScope(scope: ManagedScope): { dev: bi
|
|
|
97
115
|
|
|
98
116
|
function configuredRootPath(scope: ManagedScope): string {
|
|
99
117
|
let candidate = pathIsWithin(scope.agentDir, scope.sessionsRoot) ? scope.agentDir : path.dirname(scope.sessionsRoot);
|
|
118
|
+
const suffix: string[] = [];
|
|
100
119
|
for (;;) {
|
|
101
120
|
try {
|
|
102
121
|
const stat = fs.lstatSync(candidate);
|
|
103
122
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe configured root: ${candidate}`);
|
|
104
|
-
|
|
123
|
+
const canonical = fs.realpathSync.native(candidate);
|
|
124
|
+
return suffix.length === 0 ? canonical : path.join(canonical, ...suffix);
|
|
105
125
|
} catch (error) {
|
|
106
126
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
107
127
|
const parent = path.dirname(candidate);
|
|
108
128
|
if (parent === candidate) throw new Error("Configured managed root is unavailable.");
|
|
129
|
+
suffix.unshift(path.basename(candidate));
|
|
109
130
|
candidate = parent;
|
|
110
131
|
}
|
|
111
132
|
}
|
|
112
133
|
}
|
|
113
134
|
|
|
114
|
-
function scopeRoot(scope: ManagedScope) {
|
|
135
|
+
function scopeRoot(scope: ManagedScope, policy: ManagedSessionSecurityPolicy = "default") {
|
|
115
136
|
const bound = boundManagedWriteAuthorities.get(scope);
|
|
116
137
|
if (bound) {
|
|
117
138
|
bindManagedWriteAuthority(scope, bound);
|
|
@@ -119,7 +140,7 @@ function scopeRoot(scope: ManagedScope) {
|
|
|
119
140
|
}
|
|
120
141
|
const retained = managedRoots.get(scope);
|
|
121
142
|
if (retained) return retained;
|
|
122
|
-
const root =
|
|
143
|
+
const root = prepareManagedDirectoryRoot(configuredRootPath(scope), policy);
|
|
123
144
|
managedRoots.set(scope, root);
|
|
124
145
|
return root;
|
|
125
146
|
}
|
|
@@ -137,11 +158,20 @@ export type ManagedScopeErrorCode =
|
|
|
137
158
|
| "network_unsupported"
|
|
138
159
|
| "sessions_root_unavailable"
|
|
139
160
|
| "binding_conflict"
|
|
140
|
-
| "binding_invalid"
|
|
161
|
+
| "binding_invalid"
|
|
162
|
+
| "atomic_unavailable"
|
|
163
|
+
| "invalid_request"
|
|
164
|
+
| "durability_failed"
|
|
165
|
+
| "durability_not_provable";
|
|
141
166
|
|
|
142
167
|
export type ManagedScopeResolution =
|
|
143
168
|
| { kind: "resolved"; scope: ManagedScope }
|
|
144
|
-
| {
|
|
169
|
+
| {
|
|
170
|
+
kind: "error";
|
|
171
|
+
code: ManagedScopeErrorCode;
|
|
172
|
+
message: string;
|
|
173
|
+
cause?: { readonly classification: string; readonly diagnostic?: string };
|
|
174
|
+
};
|
|
145
175
|
|
|
146
176
|
export interface ManagedCandidate {
|
|
147
177
|
sessionId: string;
|
|
@@ -170,6 +200,9 @@ export type ManagedOpenFailure =
|
|
|
170
200
|
| "source_changed"
|
|
171
201
|
| "unsafe_artifacts"
|
|
172
202
|
| "durability_failed"
|
|
203
|
+
| "atomic_unavailable"
|
|
204
|
+
| "invalid_request"
|
|
205
|
+
| "durability_not_provable"
|
|
173
206
|
| "migration_retired"
|
|
174
207
|
| "legacy_migration_disabled"
|
|
175
208
|
| "managed_storage_unsupported";
|
|
@@ -181,6 +214,7 @@ export type ManagedOpenCandidateResult =
|
|
|
181
214
|
export type ManagedDeleteCandidateResult =
|
|
182
215
|
| { kind: "deleted"; tombstonePath: string }
|
|
183
216
|
| { kind: "already_deleted"; tombstonePath: string }
|
|
217
|
+
| { kind: "cleanup_pending"; tombstonePath: string; phase: "artifacts" | "transcript"; message: string }
|
|
184
218
|
| { kind: "error"; code: ManagedOpenFailure; message: string };
|
|
185
219
|
|
|
186
220
|
type NativeIdentity =
|
|
@@ -535,6 +569,57 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
535
569
|
}
|
|
536
570
|
}
|
|
537
571
|
|
|
572
|
+
function fsyncCanonicalBinding(bindingPath: string, expected: string): void {
|
|
573
|
+
let captured: ManagedFileSnapshot;
|
|
574
|
+
try {
|
|
575
|
+
captured = captureManagedFileNoFollow(bindingPath);
|
|
576
|
+
} catch {
|
|
577
|
+
throw new Error("binding_invalid");
|
|
578
|
+
}
|
|
579
|
+
if (captured.bytes.toString("utf8") !== expected) throw new Error("binding_invalid");
|
|
580
|
+
let descriptor: number | undefined;
|
|
581
|
+
try {
|
|
582
|
+
descriptor = fs.openSync(bindingPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
583
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
584
|
+
if (
|
|
585
|
+
before.dev !== captured.identity.dev ||
|
|
586
|
+
before.ino !== captured.identity.ino ||
|
|
587
|
+
before.size !== BigInt(captured.identity.size) ||
|
|
588
|
+
before.mtimeNs !== captured.identity.mtimeNs
|
|
589
|
+
)
|
|
590
|
+
throw new Error("binding_invalid");
|
|
591
|
+
fs.fsyncSync(descriptor);
|
|
592
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
593
|
+
if (
|
|
594
|
+
after.dev !== captured.identity.dev ||
|
|
595
|
+
after.ino !== captured.identity.ino ||
|
|
596
|
+
after.size !== BigInt(captured.identity.size) ||
|
|
597
|
+
after.mtimeNs !== captured.identity.mtimeNs
|
|
598
|
+
)
|
|
599
|
+
throw new Error("binding_invalid");
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (error instanceof Error && error.message === "binding_invalid") throw error;
|
|
602
|
+
throw new Error("durability_failed", { cause: error });
|
|
603
|
+
} finally {
|
|
604
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
605
|
+
}
|
|
606
|
+
fsyncManagedParent(bindingPath);
|
|
607
|
+
let recaptured: ManagedFileSnapshot;
|
|
608
|
+
try {
|
|
609
|
+
recaptured = captureManagedFileNoFollow(bindingPath);
|
|
610
|
+
} catch {
|
|
611
|
+
throw new Error("binding_invalid");
|
|
612
|
+
}
|
|
613
|
+
if (
|
|
614
|
+
recaptured.bytes.toString("utf8") !== expected ||
|
|
615
|
+
recaptured.identity.dev !== captured.identity.dev ||
|
|
616
|
+
recaptured.identity.ino !== captured.identity.ino ||
|
|
617
|
+
recaptured.identity.size !== captured.identity.size ||
|
|
618
|
+
recaptured.identity.mtimeNs !== captured.identity.mtimeNs
|
|
619
|
+
)
|
|
620
|
+
throw new Error("binding_invalid");
|
|
621
|
+
}
|
|
622
|
+
|
|
538
623
|
type CandidatePreflight =
|
|
539
624
|
| { kind: "capture"; identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint } }
|
|
540
625
|
| {
|
|
@@ -673,22 +758,48 @@ export async function ensureManagedScope(
|
|
|
673
758
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
674
759
|
): Promise<ManagedScopeResolution> {
|
|
675
760
|
try {
|
|
676
|
-
|
|
761
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
762
|
+
const root = scopeRoot(scope, policy);
|
|
677
763
|
ensureManagedDirectory(scope.sessionsRoot, root, policy);
|
|
678
764
|
ensureManagedDirectory(scope.directoryPath, root, policy);
|
|
679
765
|
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
680
766
|
const binding = `${JSON.stringify(bindingFor(scope))}\n`;
|
|
767
|
+
let bindingCollision = false;
|
|
681
768
|
try {
|
|
682
769
|
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
|
|
683
770
|
} catch (error) {
|
|
684
771
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
772
|
+
bindingCollision = true;
|
|
685
773
|
}
|
|
686
|
-
|
|
774
|
+
const validated = validateExistingBinding(scope);
|
|
775
|
+
if (validated) return validated;
|
|
776
|
+
if (bindingCollision) fsyncCanonicalBinding(bindingPath, binding);
|
|
777
|
+
|
|
778
|
+
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
779
|
+
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
780
|
+
throw new Error("Managed session directory changed");
|
|
781
|
+
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
782
|
+
return { kind: "resolved", scope };
|
|
687
783
|
} catch (error) {
|
|
784
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
785
|
+
const message =
|
|
786
|
+
publication?.classification ??
|
|
787
|
+
(error instanceof Error ? error.message : "The managed scope could not be initialized.");
|
|
788
|
+
const code =
|
|
789
|
+
message === "atomic_unavailable" ||
|
|
790
|
+
message === "invalid_request" ||
|
|
791
|
+
message === "durability_failed" ||
|
|
792
|
+
message === "durability_not_provable"
|
|
793
|
+
? message
|
|
794
|
+
: "binding_invalid";
|
|
795
|
+
|
|
688
796
|
return {
|
|
689
797
|
kind: "error",
|
|
690
|
-
code
|
|
691
|
-
message
|
|
798
|
+
code,
|
|
799
|
+
message,
|
|
800
|
+
...(publication
|
|
801
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
802
|
+
: { cause: { classification: code } }),
|
|
692
803
|
};
|
|
693
804
|
}
|
|
694
805
|
}
|
|
@@ -759,7 +870,8 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
759
870
|
authority?: ManagedCandidateWriteAuthority,
|
|
760
871
|
): ManagedScopeResolution {
|
|
761
872
|
try {
|
|
762
|
-
|
|
873
|
+
assertRetainedManagedDirectoryIdentity(scope);
|
|
874
|
+
const root = authority?.rootAuthority ?? scopeRoot(scope, policy);
|
|
763
875
|
if (authority) bindManagedWriteAuthority(scope, authority);
|
|
764
876
|
ensureManagedDirectory(scope.sessionsRoot, root, policy);
|
|
765
877
|
ensureManagedDirectory(scope.directoryPath, root, policy);
|
|
@@ -821,10 +933,26 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
821
933
|
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
|
|
822
934
|
return { kind: "resolved", scope };
|
|
823
935
|
} catch (error) {
|
|
936
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
937
|
+
const message =
|
|
938
|
+
publication?.classification ??
|
|
939
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
940
|
+
const code =
|
|
941
|
+
message === "atomic_unavailable" ||
|
|
942
|
+
message === "invalid_request" ||
|
|
943
|
+
message === "durability_failed" ||
|
|
944
|
+
message === "durability_not_provable"
|
|
945
|
+
? message
|
|
946
|
+
: "binding_invalid";
|
|
824
947
|
return {
|
|
825
948
|
kind: "error",
|
|
826
|
-
code
|
|
827
|
-
message
|
|
949
|
+
code,
|
|
950
|
+
message,
|
|
951
|
+
...(publication
|
|
952
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
953
|
+
: code === "binding_invalid"
|
|
954
|
+
? {}
|
|
955
|
+
: { cause: { classification: code } }),
|
|
828
956
|
};
|
|
829
957
|
}
|
|
830
958
|
}
|
|
@@ -948,6 +1076,9 @@ function expectedFailure(error: unknown): ManagedOpenFailure {
|
|
|
948
1076
|
message === "source_changed" ||
|
|
949
1077
|
message === "unsafe_artifacts" ||
|
|
950
1078
|
message === "durability_failed" ||
|
|
1079
|
+
message === "atomic_unavailable" ||
|
|
1080
|
+
message === "invalid_request" ||
|
|
1081
|
+
message === "durability_not_provable" ||
|
|
951
1082
|
message === "migration_retired"
|
|
952
1083
|
? message
|
|
953
1084
|
: "managed_storage_unsupported";
|
|
@@ -990,7 +1121,7 @@ function revalidatePickerConsent(
|
|
|
990
1121
|
function receiptPathFor(
|
|
991
1122
|
scope: ManagedScope,
|
|
992
1123
|
source: ManagedCandidate,
|
|
993
|
-
state: "prepared" | "published" | "committed" = "committed",
|
|
1124
|
+
state: "prepared" | "detached" | "published" | "committed" = "committed",
|
|
994
1125
|
): string {
|
|
995
1126
|
const suffix = state === "committed" ? "" : `.${state}`;
|
|
996
1127
|
return path.join(
|
|
@@ -1029,6 +1160,14 @@ function receiptMatches(
|
|
|
1029
1160
|
identity?: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown };
|
|
1030
1161
|
};
|
|
1031
1162
|
artifactManifest?: unknown;
|
|
1163
|
+
sourceArtifactQuarantine?: { role?: unknown };
|
|
1164
|
+
sourceArtifactCleanup?: {
|
|
1165
|
+
state?: unknown;
|
|
1166
|
+
role?: unknown;
|
|
1167
|
+
retainedPath?: unknown;
|
|
1168
|
+
identity?: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown };
|
|
1169
|
+
tree?: unknown;
|
|
1170
|
+
};
|
|
1032
1171
|
};
|
|
1033
1172
|
const exact = (
|
|
1034
1173
|
recorded: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown } | undefined,
|
|
@@ -1069,12 +1208,43 @@ function receiptMatches(
|
|
|
1069
1208
|
item.size >= 0
|
|
1070
1209
|
);
|
|
1071
1210
|
});
|
|
1211
|
+
const quarantine = record.sourceArtifactQuarantine;
|
|
1212
|
+
const cleanup = record.sourceArtifactCleanup;
|
|
1213
|
+
const cleanupIdentity = cleanup?.identity;
|
|
1214
|
+
const cleanupTree = artifactTreeSnapshot(cleanup?.tree);
|
|
1215
|
+
const validCleanup =
|
|
1216
|
+
cleanup === undefined ||
|
|
1217
|
+
(quarantine?.role === "detached_artifact_root" &&
|
|
1218
|
+
cleanup.state === "cleanup_pending" &&
|
|
1219
|
+
cleanup.role === "exchange_placeholder" &&
|
|
1220
|
+
typeof cleanup.retainedPath === "string" &&
|
|
1221
|
+
cleanupIdentity?.dev !== undefined &&
|
|
1222
|
+
cleanupIdentity.ino !== undefined &&
|
|
1223
|
+
cleanupIdentity.size !== undefined &&
|
|
1224
|
+
cleanupIdentity.mtimeNs !== undefined &&
|
|
1225
|
+
!!cleanupTree &&
|
|
1226
|
+
cleanupAuthorityMatches(
|
|
1227
|
+
{
|
|
1228
|
+
state: "cleanup_pending",
|
|
1229
|
+
role: "exchange_placeholder",
|
|
1230
|
+
retainedPath: cleanup.retainedPath,
|
|
1231
|
+
identity: {
|
|
1232
|
+
dev: BigInt(String(cleanupIdentity.dev)),
|
|
1233
|
+
ino: BigInt(String(cleanupIdentity.ino)),
|
|
1234
|
+
size: BigInt(String(cleanupIdentity.size)),
|
|
1235
|
+
mtimeNs: BigInt(String(cleanupIdentity.mtimeNs)),
|
|
1236
|
+
},
|
|
1237
|
+
tree: cleanupTree,
|
|
1238
|
+
},
|
|
1239
|
+
path.dirname(source.path),
|
|
1240
|
+
));
|
|
1072
1241
|
return (
|
|
1073
1242
|
record.schemaVersion === 2 &&
|
|
1074
1243
|
record.state === "committed" &&
|
|
1075
1244
|
record.policy === "copy-retain" &&
|
|
1076
1245
|
record.scope === scopeDigest(scope.platform, scope.canonicalCwd) &&
|
|
1077
1246
|
validManifest &&
|
|
1247
|
+
validCleanup &&
|
|
1078
1248
|
manifestContains(destination.path, manifest as readonly ArtifactManifestEntry[]) &&
|
|
1079
1249
|
record.source?.path === source.path &&
|
|
1080
1250
|
record.source?.sessionId === source.sessionId &&
|
|
@@ -1217,6 +1387,65 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1217
1387
|
* a returned partial result appends `pending(N + 1)` with the observed detached
|
|
1218
1388
|
* identity/path. Restart accepts only a contiguous, target-bound sequence.
|
|
1219
1389
|
*/
|
|
1390
|
+
type RetainedArtifactsRootReceipt = {
|
|
1391
|
+
path: string;
|
|
1392
|
+
identity: SessionStorageFileIdentity;
|
|
1393
|
+
tree: NativeDirectoryTreeSnapshot;
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
|
|
1397
|
+
const pathname = deterministicRemovalRoot(receipt.plannedArtifactsPath);
|
|
1398
|
+
if (!fs.existsSync(pathname)) return undefined;
|
|
1399
|
+
if (!receipt.expectedArtifactsIdentity) throw new Error("durability_failed");
|
|
1400
|
+
const identity = artifactIdentityAt(pathname);
|
|
1401
|
+
if (!identity || !sameArtifactRootIdentity(identity, receipt.expectedArtifactsIdentity))
|
|
1402
|
+
throw new Error("durability_failed");
|
|
1403
|
+
const tree = snapshotArtifactTree(pathname);
|
|
1404
|
+
return { path: pathname, identity, tree };
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean {
|
|
1408
|
+
if (record.retainedArtifactsRoot === undefined) return true;
|
|
1409
|
+
if (!record.retainedArtifactsRoot || typeof record.retainedArtifactsRoot !== "object")
|
|
1410
|
+
throw new Error("durability_failed");
|
|
1411
|
+
const retained = record.retainedArtifactsRoot as Record<string, unknown>;
|
|
1412
|
+
const identity = retained.identity;
|
|
1413
|
+
const tree = artifactTreeSnapshot(retained.tree);
|
|
1414
|
+
if (
|
|
1415
|
+
typeof record.plannedArtifactsPath !== "string" ||
|
|
1416
|
+
retained.path !== deterministicRemovalRoot(record.plannedArtifactsPath) ||
|
|
1417
|
+
!identity ||
|
|
1418
|
+
typeof identity !== "object" ||
|
|
1419
|
+
Array.isArray(identity) ||
|
|
1420
|
+
!tree
|
|
1421
|
+
)
|
|
1422
|
+
throw new Error("durability_failed");
|
|
1423
|
+
const artifactIdentity = identity as Record<string, unknown>;
|
|
1424
|
+
if (
|
|
1425
|
+
typeof artifactIdentity.dev !== "string" ||
|
|
1426
|
+
typeof artifactIdentity.ino !== "string" ||
|
|
1427
|
+
typeof artifactIdentity.size !== "number" ||
|
|
1428
|
+
typeof artifactIdentity.mtimeNs !== "string" ||
|
|
1429
|
+
typeof artifactIdentity.sha256 !== "string"
|
|
1430
|
+
)
|
|
1431
|
+
throw new Error("durability_failed");
|
|
1432
|
+
if (!fs.existsSync(retained.path)) return true;
|
|
1433
|
+
const observed = artifactIdentityAt(retained.path);
|
|
1434
|
+
if (
|
|
1435
|
+
!observed ||
|
|
1436
|
+
!sameArtifactRootIdentity(observed, {
|
|
1437
|
+
dev: BigInt(artifactIdentity.dev),
|
|
1438
|
+
ino: BigInt(artifactIdentity.ino),
|
|
1439
|
+
size: artifactIdentity.size,
|
|
1440
|
+
mtimeNs: BigInt(artifactIdentity.mtimeNs),
|
|
1441
|
+
sha256: artifactIdentity.sha256,
|
|
1442
|
+
}) ||
|
|
1443
|
+
JSON.stringify(snapshotArtifactTree(retained.path)) !== JSON.stringify(tree)
|
|
1444
|
+
)
|
|
1445
|
+
throw new Error("durability_failed");
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1220
1449
|
type CleanupReceipt = {
|
|
1221
1450
|
attempt: number;
|
|
1222
1451
|
target: RetiredTarget;
|
|
@@ -1224,6 +1453,13 @@ type CleanupReceipt = {
|
|
|
1224
1453
|
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
1225
1454
|
detachedArtifactsPath?: string;
|
|
1226
1455
|
detachedTranscriptPath?: string;
|
|
1456
|
+
retainedArtifactsSuccessorPath?: string;
|
|
1457
|
+
retainedArtifactsPlaceholderPath?: string;
|
|
1458
|
+
retainedArtifactsUnknownPath?: string;
|
|
1459
|
+
retainedTranscriptSuccessorPath?: string;
|
|
1460
|
+
retainedTranscriptPlaceholderPath?: string;
|
|
1461
|
+
retainedTranscriptUnknownPath?: string;
|
|
1462
|
+
|
|
1227
1463
|
plannedArtifactsPath: string;
|
|
1228
1464
|
plannedTranscriptPath: string;
|
|
1229
1465
|
};
|
|
@@ -1257,6 +1493,24 @@ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: Cleanup
|
|
|
1257
1493
|
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
1258
1494
|
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
1259
1495
|
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
1496
|
+
...(receipt.retainedArtifactsSuccessorPath
|
|
1497
|
+
? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
|
|
1498
|
+
: {}),
|
|
1499
|
+
...(receipt.retainedArtifactsPlaceholderPath
|
|
1500
|
+
? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
|
|
1501
|
+
: {}),
|
|
1502
|
+
...(receipt.retainedArtifactsUnknownPath
|
|
1503
|
+
? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
|
|
1504
|
+
: {}),
|
|
1505
|
+
...(receipt.retainedTranscriptSuccessorPath
|
|
1506
|
+
? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
|
|
1507
|
+
: {}),
|
|
1508
|
+
...(receipt.retainedTranscriptPlaceholderPath
|
|
1509
|
+
? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
|
|
1510
|
+
: {}),
|
|
1511
|
+
...(receipt.retainedTranscriptUnknownPath
|
|
1512
|
+
? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
|
|
1513
|
+
: {}),
|
|
1260
1514
|
plannedArtifactsPath: receipt.plannedArtifactsPath,
|
|
1261
1515
|
plannedTranscriptPath: receipt.plannedTranscriptPath,
|
|
1262
1516
|
};
|
|
@@ -1277,6 +1531,7 @@ function cleanupArtifactsRemoved(
|
|
|
1277
1531
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1278
1532
|
const record = value as Record<string, unknown>;
|
|
1279
1533
|
const recorded = record.target as Record<string, unknown> | undefined;
|
|
1534
|
+
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
1280
1535
|
return (
|
|
1281
1536
|
record.schemaVersion === 2 &&
|
|
1282
1537
|
record.state === "artifacts_removed" &&
|
|
@@ -1285,9 +1540,17 @@ function cleanupArtifactsRemoved(
|
|
|
1285
1540
|
record.attempt === attempt &&
|
|
1286
1541
|
recorded?.path === target.path &&
|
|
1287
1542
|
recorded.sessionId === target.sessionId &&
|
|
1288
|
-
recorded.cwd === target.cwd
|
|
1543
|
+
recorded.cwd === target.cwd &&
|
|
1544
|
+
identity?.canonicalPath === target.identity.canonicalPath &&
|
|
1545
|
+
identity.dev === String(target.identity.dev) &&
|
|
1546
|
+
identity.ino === String(target.identity.ino) &&
|
|
1547
|
+
identity.size === target.identity.size &&
|
|
1548
|
+
identity.mtimeNs === String(target.identity.mtimeNs) &&
|
|
1549
|
+
identity.sha256 === target.identity.sha256 &&
|
|
1550
|
+
retainedArtifactsRootMatches(record)
|
|
1289
1551
|
);
|
|
1290
|
-
} catch {
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
if ((error as Error).message === "durability_failed") throw error;
|
|
1291
1554
|
return false;
|
|
1292
1555
|
}
|
|
1293
1556
|
}
|
|
@@ -1298,9 +1561,14 @@ async function publishCleanupArtifactsRemoved(
|
|
|
1298
1561
|
receipt: CleanupReceipt,
|
|
1299
1562
|
lock: ManagedStorageLock,
|
|
1300
1563
|
): Promise<void> {
|
|
1564
|
+
const retainedArtifactsRoot = retainedArtifactsRootReceipt(receipt);
|
|
1301
1565
|
await publishManagedTombstone(
|
|
1302
1566
|
cleanupReceiptPath(tombstone, receipt.target, "artifacts_removed", receipt.attempt),
|
|
1303
|
-
{
|
|
1567
|
+
{
|
|
1568
|
+
...cleanupReceipt(scope, tombstone, receipt),
|
|
1569
|
+
state: "artifacts_removed",
|
|
1570
|
+
...(retainedArtifactsRoot ? { retainedArtifactsRoot } : {}),
|
|
1571
|
+
},
|
|
1304
1572
|
lock.assertOwned,
|
|
1305
1573
|
).catch(error => {
|
|
1306
1574
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
@@ -1317,6 +1585,14 @@ function isQuarantinePath(target: RetiredTarget, pathname: unknown): pathname is
|
|
|
1317
1585
|
);
|
|
1318
1586
|
}
|
|
1319
1587
|
|
|
1588
|
+
function isRetainedNativePath(target: RetiredTarget, pathname: unknown): pathname is string {
|
|
1589
|
+
return (
|
|
1590
|
+
typeof pathname === "string" &&
|
|
1591
|
+
path.dirname(pathname) === path.dirname(target.path) &&
|
|
1592
|
+
path.basename(pathname).startsWith(".gjc-")
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1320
1596
|
function deterministicRemovalRoot(plannedRoot: string): string {
|
|
1321
1597
|
return `${plannedRoot}.removing`;
|
|
1322
1598
|
}
|
|
@@ -1328,17 +1604,44 @@ function isAuthorizedArtifactRoot(target: RetiredTarget, plannedRoot: string, pa
|
|
|
1328
1604
|
);
|
|
1329
1605
|
}
|
|
1330
1606
|
|
|
1331
|
-
function
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1607
|
+
function transcriptRootMatchesTarget(pathname: string, target: RetiredTarget): boolean {
|
|
1608
|
+
try {
|
|
1609
|
+
const observed = captureManagedFileNoFollow(pathname);
|
|
1610
|
+
const digest = createHash("sha256").update(observed.bytes).digest("hex");
|
|
1611
|
+
return (
|
|
1612
|
+
observed.identity.dev === target.identity.dev &&
|
|
1613
|
+
observed.identity.ino === target.identity.ino &&
|
|
1614
|
+
observed.identity.size === target.identity.size &&
|
|
1615
|
+
observed.identity.mtimeNs === target.identity.mtimeNs &&
|
|
1616
|
+
digest === target.identity.sha256
|
|
1617
|
+
);
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
1620
|
+
throw new Error("durability_failed");
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function cleanupRootsAbsent(tombstone: string, target: RetiredTarget, pending: CleanupReceipt): boolean {
|
|
1337
1625
|
const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
|
|
1626
|
+
const activeTranscriptRoots = new Set(
|
|
1627
|
+
[pending.plannedTranscriptPath, pending.detachedTranscriptPath].filter((pathname): pathname is string =>
|
|
1628
|
+
isQuarantinePath(target, pathname),
|
|
1629
|
+
),
|
|
1630
|
+
);
|
|
1338
1631
|
const roots = new Set<string>([
|
|
1632
|
+
target.path,
|
|
1633
|
+
target.path.slice(0, -6),
|
|
1339
1634
|
pending.plannedArtifactsPath,
|
|
1340
1635
|
deterministicRemovalRoot(pending.plannedArtifactsPath),
|
|
1341
1636
|
...(pending.detachedArtifactsPath ? [pending.detachedArtifactsPath] : []),
|
|
1637
|
+
...[
|
|
1638
|
+
pending.retainedArtifactsSuccessorPath,
|
|
1639
|
+
pending.retainedArtifactsPlaceholderPath,
|
|
1640
|
+
pending.retainedArtifactsUnknownPath,
|
|
1641
|
+
pending.retainedTranscriptSuccessorPath,
|
|
1642
|
+
pending.retainedTranscriptPlaceholderPath,
|
|
1643
|
+
pending.retainedTranscriptUnknownPath,
|
|
1644
|
+
].filter((pathname): pathname is string => isRetainedNativePath(target, pathname)),
|
|
1342
1645
|
]);
|
|
1343
1646
|
for (const name of fs.readdirSync(path.dirname(tombstone))) {
|
|
1344
1647
|
if (!name.startsWith(prefix) || !name.endsWith(".json")) continue;
|
|
@@ -1347,26 +1650,91 @@ function artifactRootsAbsent(
|
|
|
1347
1650
|
) as {
|
|
1348
1651
|
plannedArtifactsPath?: unknown;
|
|
1349
1652
|
detachedArtifactsPath?: unknown;
|
|
1653
|
+
plannedTranscriptPath?: unknown;
|
|
1654
|
+
detachedTranscriptPath?: unknown;
|
|
1655
|
+
retainedArtifactsSuccessorPath?: unknown;
|
|
1656
|
+
retainedArtifactsPlaceholderPath?: unknown;
|
|
1657
|
+
retainedArtifactsUnknownPath?: unknown;
|
|
1658
|
+
retainedTranscriptSuccessorPath?: unknown;
|
|
1659
|
+
retainedTranscriptPlaceholderPath?: unknown;
|
|
1660
|
+
retainedTranscriptUnknownPath?: unknown;
|
|
1350
1661
|
};
|
|
1351
1662
|
if (isQuarantinePath(target, record.plannedArtifactsPath)) {
|
|
1352
1663
|
roots.add(record.plannedArtifactsPath);
|
|
1353
1664
|
roots.add(deterministicRemovalRoot(record.plannedArtifactsPath));
|
|
1354
1665
|
}
|
|
1355
1666
|
if (isQuarantinePath(target, record.detachedArtifactsPath)) roots.add(record.detachedArtifactsPath);
|
|
1667
|
+
for (const pathname of [record.plannedTranscriptPath, record.detachedTranscriptPath]) {
|
|
1668
|
+
if (isQuarantinePath(target, pathname) && !activeTranscriptRoots.has(pathname)) roots.add(pathname);
|
|
1669
|
+
}
|
|
1670
|
+
for (const pathname of [
|
|
1671
|
+
record.retainedArtifactsSuccessorPath,
|
|
1672
|
+
record.retainedArtifactsPlaceholderPath,
|
|
1673
|
+
record.retainedArtifactsUnknownPath,
|
|
1674
|
+
record.retainedTranscriptSuccessorPath,
|
|
1675
|
+
record.retainedTranscriptPlaceholderPath,
|
|
1676
|
+
record.retainedTranscriptUnknownPath,
|
|
1677
|
+
]) {
|
|
1678
|
+
if (isRetainedNativePath(target, pathname)) roots.add(pathname);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
for (const pathname of roots) {
|
|
1682
|
+
try {
|
|
1683
|
+
fs.lstatSync(pathname);
|
|
1684
|
+
throw new Error("durability_failed");
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1687
|
+
throw error;
|
|
1688
|
+
}
|
|
1356
1689
|
}
|
|
1357
|
-
|
|
1690
|
+
for (const pathname of activeTranscriptRoots) {
|
|
1691
|
+
try {
|
|
1692
|
+
fs.lstatSync(pathname);
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1695
|
+
throw error;
|
|
1696
|
+
}
|
|
1697
|
+
if (!transcriptRootMatchesTarget(pathname, target)) throw new Error("durability_failed");
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
return true;
|
|
1358
1701
|
}
|
|
1359
1702
|
|
|
1360
1703
|
function sameArtifactRootIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
|
|
1361
1704
|
return left.dev === right.dev && left.ino === right.ino;
|
|
1362
1705
|
}
|
|
1363
1706
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1707
|
+
function sameArtifactTreeSnapshot(left: NativeDirectoryTreeSnapshot, right: NativeDirectoryTreeSnapshot): boolean {
|
|
1708
|
+
if (left.rootDev !== right.rootDev || left.rootIno !== right.rootIno || left.entries.length !== right.entries.length)
|
|
1709
|
+
return false;
|
|
1710
|
+
const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string =>
|
|
1711
|
+
JSON.stringify([entry.relativePath, entry.kind, entry.dev, entry.ino, entry.size, entry.mtimeNs, entry.sha256]);
|
|
1712
|
+
const leftEntries = left.entries.map(entryKey).sort();
|
|
1713
|
+
const rightEntries = right.entries.map(entryKey).sort();
|
|
1714
|
+
return leftEntries.every((entry, index) => entry === rightEntries[index]);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
1718
|
+
if (!pending.detachedArtifactsPath) return;
|
|
1719
|
+
if (!pending.expectedArtifactsIdentity || !pending.expectedArtifactsTree) throw new Error("durability_failed");
|
|
1720
|
+
try {
|
|
1721
|
+
fs.lstatSync(pending.detachedArtifactsPath);
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
1724
|
+
throw new Error("durability_failed");
|
|
1725
|
+
}
|
|
1726
|
+
const observed = artifactIdentityAt(pending.detachedArtifactsPath);
|
|
1727
|
+
if (
|
|
1728
|
+
!observed ||
|
|
1729
|
+
!sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
|
|
1730
|
+
!sameArtifactTreeSnapshot(snapshotArtifactTree(pending.detachedArtifactsPath), pending.expectedArtifactsTree)
|
|
1731
|
+
)
|
|
1732
|
+
throw new Error("binding_invalid");
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1369
1735
|
function probePlannedCleanupDetach(target: RetiredTarget, pending: CleanupReceipt): CleanupReceipt {
|
|
1736
|
+
assertRetainedArtifactsAuthority(pending);
|
|
1737
|
+
|
|
1370
1738
|
let detachedArtifactsPath = pending.detachedArtifactsPath;
|
|
1371
1739
|
let detachedTranscriptPath = pending.detachedTranscriptPath;
|
|
1372
1740
|
for (const pathname of [pending.plannedArtifactsPath, deterministicRemovalRoot(pending.plannedArtifactsPath)]) {
|
|
@@ -1430,6 +1798,16 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
1430
1798
|
})
|
|
1431
1799
|
)
|
|
1432
1800
|
return undefined;
|
|
1801
|
+
const roots = snapshot.entries.filter(entry => {
|
|
1802
|
+
const item = entry as Record<string, unknown>;
|
|
1803
|
+
return (
|
|
1804
|
+
item.relativePath === "" &&
|
|
1805
|
+
item.kind === "directory" &&
|
|
1806
|
+
item.dev === snapshot.rootDev &&
|
|
1807
|
+
item.ino === snapshot.rootIno
|
|
1808
|
+
);
|
|
1809
|
+
});
|
|
1810
|
+
if (roots.length !== 1) return undefined;
|
|
1433
1811
|
return snapshot as unknown as NativeDirectoryTreeSnapshot;
|
|
1434
1812
|
}
|
|
1435
1813
|
|
|
@@ -1481,7 +1859,19 @@ function pendingCleanupReceipt(
|
|
|
1481
1859
|
plannedPaths.has(record.plannedArtifactsPath as string) ||
|
|
1482
1860
|
plannedPaths.has(record.plannedTranscriptPath as string) ||
|
|
1483
1861
|
(record.detachedArtifactsPath !== undefined && !isQuarantinePath(target, record.detachedArtifactsPath)) ||
|
|
1484
|
-
(record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath))
|
|
1862
|
+
(record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath)) ||
|
|
1863
|
+
(record.retainedArtifactsSuccessorPath !== undefined &&
|
|
1864
|
+
!isRetainedNativePath(target, record.retainedArtifactsSuccessorPath)) ||
|
|
1865
|
+
(record.retainedArtifactsPlaceholderPath !== undefined &&
|
|
1866
|
+
!isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) ||
|
|
1867
|
+
(record.retainedArtifactsUnknownPath !== undefined &&
|
|
1868
|
+
!isRetainedNativePath(target, record.retainedArtifactsUnknownPath)) ||
|
|
1869
|
+
(record.retainedTranscriptSuccessorPath !== undefined &&
|
|
1870
|
+
!isRetainedNativePath(target, record.retainedTranscriptSuccessorPath)) ||
|
|
1871
|
+
(record.retainedTranscriptPlaceholderPath !== undefined &&
|
|
1872
|
+
!isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
|
|
1873
|
+
(record.retainedTranscriptUnknownPath !== undefined &&
|
|
1874
|
+
!isRetainedNativePath(target, record.retainedTranscriptUnknownPath))
|
|
1485
1875
|
)
|
|
1486
1876
|
throw new Error("durability_failed");
|
|
1487
1877
|
const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
|
|
@@ -1504,7 +1894,7 @@ function pendingCleanupReceipt(
|
|
|
1504
1894
|
const expectedArtifactsTree =
|
|
1505
1895
|
record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
|
|
1506
1896
|
if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
|
|
1507
|
-
|
|
1897
|
+
|
|
1508
1898
|
if (
|
|
1509
1899
|
latest &&
|
|
1510
1900
|
((record.detachedArtifactsPath !== undefined &&
|
|
@@ -1523,6 +1913,12 @@ function pendingCleanupReceipt(
|
|
|
1523
1913
|
expectedArtifactsTree,
|
|
1524
1914
|
detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
|
|
1525
1915
|
detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
|
|
1916
|
+
retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
|
|
1917
|
+
retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
|
|
1918
|
+
retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
|
|
1919
|
+
retainedTranscriptSuccessorPath: record.retainedTranscriptSuccessorPath as string | undefined,
|
|
1920
|
+
retainedTranscriptPlaceholderPath: record.retainedTranscriptPlaceholderPath as string | undefined,
|
|
1921
|
+
retainedTranscriptUnknownPath: record.retainedTranscriptUnknownPath as string | undefined,
|
|
1526
1922
|
plannedArtifactsPath: record.plannedArtifactsPath as string,
|
|
1527
1923
|
plannedTranscriptPath: record.plannedTranscriptPath as string,
|
|
1528
1924
|
};
|
|
@@ -1572,7 +1968,13 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
|
|
|
1572
1968
|
expectedArtifactsIdentity,
|
|
1573
1969
|
expectedArtifactsTree,
|
|
1574
1970
|
detachedArtifactsPath: pending?.detachedArtifactsPath,
|
|
1971
|
+
retainedArtifactsSuccessorPath: pending?.retainedArtifactsSuccessorPath,
|
|
1972
|
+
retainedArtifactsPlaceholderPath: pending?.retainedArtifactsPlaceholderPath,
|
|
1973
|
+
retainedArtifactsUnknownPath: pending?.retainedArtifactsUnknownPath,
|
|
1575
1974
|
detachedTranscriptPath: pending?.detachedTranscriptPath,
|
|
1975
|
+
retainedTranscriptSuccessorPath: pending?.retainedTranscriptSuccessorPath,
|
|
1976
|
+
retainedTranscriptPlaceholderPath: pending?.retainedTranscriptPlaceholderPath,
|
|
1977
|
+
retainedTranscriptUnknownPath: pending?.retainedTranscriptUnknownPath,
|
|
1576
1978
|
plannedArtifactsPath: path.join(directory, `.gjc-delete-${operation}-artifacts-${attempt}`),
|
|
1577
1979
|
plannedTranscriptPath: path.join(directory, `.gjc-delete-${operation}-transcript-${attempt}`),
|
|
1578
1980
|
};
|
|
@@ -1585,6 +1987,36 @@ function requiresFreshCleanupPlan(pending: CleanupReceipt): boolean {
|
|
|
1585
1987
|
);
|
|
1586
1988
|
}
|
|
1587
1989
|
|
|
1990
|
+
/** Evidence-carrier for a verified `cleanup_pending` deletion, bound to the active plan. */
|
|
1991
|
+
function cleanupPendingEvidence(
|
|
1992
|
+
retry: CleanupReceipt,
|
|
1993
|
+
active: CleanupReceipt,
|
|
1994
|
+
deletion: Extract<VerifiedSessionDeleteResult, { kind: "cleanup_pending" }>,
|
|
1995
|
+
): CleanupReceipt {
|
|
1996
|
+
return {
|
|
1997
|
+
...retry,
|
|
1998
|
+
expectedArtifactsIdentity:
|
|
1999
|
+
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
2000
|
+
expectedArtifactsTree: deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2001
|
+
detachedArtifactsPath:
|
|
2002
|
+
deletion.phase === "artifacts" ? deletion.detachedArtifactsPath : active.detachedArtifactsPath,
|
|
2003
|
+
detachedTranscriptPath:
|
|
2004
|
+
deletion.phase === "transcript" ? deletion.detachedTranscriptPath : active.detachedTranscriptPath,
|
|
2005
|
+
retainedArtifactsSuccessorPath:
|
|
2006
|
+
deletion.phase === "artifacts" ? deletion.retainedSuccessorPath : active.retainedArtifactsSuccessorPath,
|
|
2007
|
+
retainedArtifactsPlaceholderPath:
|
|
2008
|
+
deletion.phase === "artifacts" ? deletion.retainedPlaceholderPath : active.retainedArtifactsPlaceholderPath,
|
|
2009
|
+
retainedArtifactsUnknownPath:
|
|
2010
|
+
deletion.phase === "artifacts" ? deletion.retainedUnknownPath : active.retainedArtifactsUnknownPath,
|
|
2011
|
+
retainedTranscriptSuccessorPath:
|
|
2012
|
+
deletion.phase === "transcript" ? deletion.retainedSuccessorPath : active.retainedTranscriptSuccessorPath,
|
|
2013
|
+
retainedTranscriptPlaceholderPath:
|
|
2014
|
+
deletion.phase === "transcript" ? deletion.retainedPlaceholderPath : active.retainedTranscriptPlaceholderPath,
|
|
2015
|
+
retainedTranscriptUnknownPath:
|
|
2016
|
+
deletion.phase === "transcript" ? deletion.retainedUnknownPath : active.retainedTranscriptUnknownPath,
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1588
2020
|
async function publishCleanupPending(
|
|
1589
2021
|
scope: ManagedScope,
|
|
1590
2022
|
tombstone: string,
|
|
@@ -1872,13 +2304,17 @@ function sameDirectoryObject(leftPath: string, rightPath: string): boolean {
|
|
|
1872
2304
|
function matchesDetachedArtifactRoot(pathname: string, plan: DetachedArtifactRoot): boolean {
|
|
1873
2305
|
try {
|
|
1874
2306
|
const stat = fs.lstatSync(pathname, { bigint: true });
|
|
2307
|
+
const snapshot = native.snapshotDirectoryTree(pathname);
|
|
2308
|
+
const root = snapshot.snapshot?.entries.find(entry => entry.relativePath === "" && entry.kind === "directory");
|
|
1875
2309
|
return (
|
|
1876
2310
|
stat.isDirectory() &&
|
|
1877
2311
|
!stat.isSymbolicLink() &&
|
|
1878
2312
|
stat.dev === plan.identity.dev &&
|
|
1879
2313
|
stat.ino === plan.identity.ino &&
|
|
1880
|
-
stat.size === plan.identity.size &&
|
|
1881
2314
|
stat.mtimeNs === plan.identity.mtimeNs &&
|
|
2315
|
+
(process.platform === "win32"
|
|
2316
|
+
? snapshot.ok && !!root && BigInt(root.size) === plan.identity.size
|
|
2317
|
+
: stat.size === plan.identity.size) &&
|
|
1882
2318
|
sameDirectoryObject(path.dirname(pathname), path.dirname(plan.detachedPath))
|
|
1883
2319
|
);
|
|
1884
2320
|
} catch {
|
|
@@ -1886,29 +2322,130 @@ function matchesDetachedArtifactRoot(pathname: string, plan: DetachedArtifactRoo
|
|
|
1886
2322
|
}
|
|
1887
2323
|
}
|
|
1888
2324
|
|
|
1889
|
-
function
|
|
2325
|
+
function sameArtifactTree(left: NativeDirectoryTreeSnapshot, right: NativeDirectoryTreeSnapshot): boolean {
|
|
2326
|
+
const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string =>
|
|
2327
|
+
JSON.stringify([
|
|
2328
|
+
entry.relativePath,
|
|
2329
|
+
entry.kind,
|
|
2330
|
+
entry.dev,
|
|
2331
|
+
entry.ino,
|
|
2332
|
+
entry.size,
|
|
2333
|
+
entry.mtimeNs,
|
|
2334
|
+
entry.ctimeNs,
|
|
2335
|
+
entry.sha256,
|
|
2336
|
+
]);
|
|
2337
|
+
return (
|
|
2338
|
+
left.rootDev === right.rootDev &&
|
|
2339
|
+
left.rootIno === right.rootIno &&
|
|
2340
|
+
left.entries.length === right.entries.length &&
|
|
2341
|
+
left.entries
|
|
2342
|
+
.map(entryKey)
|
|
2343
|
+
.sort()
|
|
2344
|
+
.every((entry, index) => entry === right.entries.map(entryKey).sort()[index])
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
type SourceArtifactCleanup = {
|
|
2349
|
+
state: "cleanup_pending";
|
|
2350
|
+
role: "exchange_placeholder";
|
|
2351
|
+
retainedPath: string;
|
|
2352
|
+
identity: DetachedArtifactRoot["identity"];
|
|
2353
|
+
tree: NativeDirectoryTreeSnapshot;
|
|
2354
|
+
};
|
|
2355
|
+
|
|
2356
|
+
function cleanupAuthorityMatches(cleanup: SourceArtifactCleanup, parent: string): boolean {
|
|
2357
|
+
try {
|
|
2358
|
+
const stat = fs.lstatSync(cleanup.retainedPath, { bigint: true });
|
|
2359
|
+
if (
|
|
2360
|
+
path.dirname(cleanup.retainedPath) !== parent ||
|
|
2361
|
+
!stat.isDirectory() ||
|
|
2362
|
+
stat.isSymbolicLink() ||
|
|
2363
|
+
stat.dev !== cleanup.identity.dev ||
|
|
2364
|
+
stat.ino !== cleanup.identity.ino ||
|
|
2365
|
+
stat.size !== cleanup.identity.size ||
|
|
2366
|
+
stat.mtimeNs !== cleanup.identity.mtimeNs
|
|
2367
|
+
)
|
|
2368
|
+
return false;
|
|
2369
|
+
const snapshot = native.snapshotDirectoryTree(cleanup.retainedPath);
|
|
2370
|
+
return (
|
|
2371
|
+
!!snapshot.ok &&
|
|
2372
|
+
!!snapshot.snapshot &&
|
|
2373
|
+
sameArtifactTree(snapshot.snapshot, cleanup.tree) &&
|
|
2374
|
+
cleanup.tree.entries.length === 1 &&
|
|
2375
|
+
cleanup.tree.entries[0]?.relativePath === "" &&
|
|
2376
|
+
cleanup.tree.entries[0]?.kind === "directory"
|
|
2377
|
+
);
|
|
2378
|
+
} catch {
|
|
2379
|
+
return false;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function detachArtifactRootForMigration(plan: DetachedArtifactRoot): {
|
|
2384
|
+
detached: DetachedArtifactRoot;
|
|
2385
|
+
cleanup?: SourceArtifactCleanup;
|
|
2386
|
+
} {
|
|
1890
2387
|
const result = native.exactUnlink(plan.originalPath, {
|
|
1891
2388
|
...plan.identity,
|
|
1892
2389
|
directory: true,
|
|
1893
2390
|
detachOnly: true,
|
|
1894
2391
|
quarantineName: path.basename(plan.detachedPath),
|
|
1895
2392
|
});
|
|
1896
|
-
|
|
2393
|
+
const cleanSuccess =
|
|
2394
|
+
result.ok && !result.retainedSuccessorPath && !result.retainedPlaceholderPath && !result.retainedUnknownPath;
|
|
2395
|
+
const cleanupPending =
|
|
2396
|
+
!result.ok &&
|
|
2397
|
+
result.code === "cleanup_pending" &&
|
|
2398
|
+
!!result.detachedPath &&
|
|
2399
|
+
!!result.retainedPlaceholderPath &&
|
|
2400
|
+
!result.retainedSuccessorPath &&
|
|
2401
|
+
!result.retainedUnknownPath;
|
|
2402
|
+
if (
|
|
2403
|
+
(!cleanSuccess && !cleanupPending) ||
|
|
2404
|
+
!result.detachedPath ||
|
|
2405
|
+
!matchesDetachedArtifactRoot(result.detachedPath, plan)
|
|
2406
|
+
)
|
|
1897
2407
|
throw new Error("durability_failed");
|
|
2408
|
+
|
|
1898
2409
|
const detachedPath =
|
|
1899
2410
|
process.platform === "win32" ? fs.realpathSync.native(result.detachedPath) : result.detachedPath;
|
|
1900
2411
|
if (!matchesDetachedArtifactRoot(detachedPath, plan)) throw new Error("durability_failed");
|
|
1901
|
-
|
|
2412
|
+
const detached = { ...plan, detachedPath };
|
|
2413
|
+
if (!cleanupPending) return { detached };
|
|
2414
|
+
const placeholder = result.retainedPlaceholderPath!;
|
|
2415
|
+
const stat = fs.lstatSync(placeholder, { bigint: true });
|
|
2416
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || path.dirname(placeholder) !== path.dirname(plan.originalPath))
|
|
2417
|
+
throw new Error("durability_failed");
|
|
2418
|
+
const snapshot = native.snapshotDirectoryTree(placeholder);
|
|
2419
|
+
const cleanup: SourceArtifactCleanup = {
|
|
2420
|
+
state: "cleanup_pending",
|
|
2421
|
+
role: "exchange_placeholder",
|
|
2422
|
+
retainedPath: placeholder,
|
|
2423
|
+
identity: { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeNs: stat.mtimeNs },
|
|
2424
|
+
tree: snapshot.snapshot!,
|
|
2425
|
+
};
|
|
2426
|
+
if (!snapshot.ok || !snapshot.snapshot || !cleanupAuthorityMatches(cleanup, path.dirname(plan.originalPath)))
|
|
2427
|
+
throw new Error("durability_failed");
|
|
2428
|
+
return { detached, cleanup };
|
|
1902
2429
|
}
|
|
1903
2430
|
|
|
1904
2431
|
function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandidate): void {
|
|
1905
|
-
const
|
|
2432
|
+
const preparedReceipt = receiptPathFor(scope, source, "prepared");
|
|
2433
|
+
const detachedReceipt = receiptPathFor(scope, source, "detached");
|
|
2434
|
+
const receipt = fs.existsSync(detachedReceipt) ? detachedReceipt : preparedReceipt;
|
|
1906
2435
|
let record: {
|
|
1907
2436
|
sourceArtifactQuarantine?: {
|
|
1908
2437
|
path?: unknown;
|
|
1909
2438
|
detachedPath?: unknown;
|
|
1910
2439
|
identity?: Record<string, unknown>;
|
|
1911
2440
|
tree?: unknown;
|
|
2441
|
+
role?: unknown;
|
|
2442
|
+
};
|
|
2443
|
+
sourceArtifactCleanup?: {
|
|
2444
|
+
state?: unknown;
|
|
2445
|
+
role?: unknown;
|
|
2446
|
+
retainedPath?: unknown;
|
|
2447
|
+
identity?: Record<string, unknown>;
|
|
2448
|
+
tree?: unknown;
|
|
1912
2449
|
};
|
|
1913
2450
|
};
|
|
1914
2451
|
try {
|
|
@@ -1933,25 +2470,62 @@ function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandida
|
|
|
1933
2470
|
typeof identity.mtimeNs !== "string"
|
|
1934
2471
|
)
|
|
1935
2472
|
throw new Error("durability_failed");
|
|
2473
|
+
if (receipt === detachedReceipt && quarantine.role !== "detached_artifact_root")
|
|
2474
|
+
throw new Error("durability_failed");
|
|
2475
|
+
if (receipt === detachedReceipt) {
|
|
2476
|
+
const cleanup = record.sourceArtifactCleanup;
|
|
2477
|
+
const cleanupIdentity = cleanup?.identity;
|
|
2478
|
+
const cleanupTree = artifactTreeSnapshot(cleanup?.tree);
|
|
2479
|
+
if (
|
|
2480
|
+
cleanup?.state !== "cleanup_pending" ||
|
|
2481
|
+
cleanup.role !== "exchange_placeholder" ||
|
|
2482
|
+
typeof cleanup.retainedPath !== "string" ||
|
|
2483
|
+
!cleanupIdentity ||
|
|
2484
|
+
typeof cleanupIdentity.dev !== "string" ||
|
|
2485
|
+
typeof cleanupIdentity.ino !== "string" ||
|
|
2486
|
+
typeof cleanupIdentity.size !== "string" ||
|
|
2487
|
+
typeof cleanupIdentity.mtimeNs !== "string" ||
|
|
2488
|
+
!cleanupTree ||
|
|
2489
|
+
!cleanupAuthorityMatches(
|
|
2490
|
+
{
|
|
2491
|
+
state: "cleanup_pending",
|
|
2492
|
+
role: "exchange_placeholder",
|
|
2493
|
+
retainedPath: cleanup.retainedPath,
|
|
2494
|
+
identity: {
|
|
2495
|
+
dev: BigInt(cleanupIdentity.dev),
|
|
2496
|
+
ino: BigInt(cleanupIdentity.ino),
|
|
2497
|
+
size: BigInt(cleanupIdentity.size),
|
|
2498
|
+
mtimeNs: BigInt(cleanupIdentity.mtimeNs),
|
|
2499
|
+
},
|
|
2500
|
+
tree: cleanupTree,
|
|
2501
|
+
},
|
|
2502
|
+
path.dirname(source.path),
|
|
2503
|
+
)
|
|
2504
|
+
)
|
|
2505
|
+
throw new Error("durability_failed");
|
|
2506
|
+
}
|
|
2507
|
+
|
|
1936
2508
|
const expectedTree = artifactTreeSnapshot(quarantine.tree)!;
|
|
1937
2509
|
const assertPreparedTree = (pathname: string): void => {
|
|
1938
2510
|
validateManagedArtifactTree(pathname);
|
|
1939
2511
|
const observed = native.snapshotDirectoryTree(pathname);
|
|
1940
|
-
if (!observed.ok || !observed.snapshot ||
|
|
2512
|
+
if (!observed.ok || !observed.snapshot || !sameArtifactTree(observed.snapshot, expectedTree))
|
|
1941
2513
|
throw new Error("durability_failed");
|
|
1942
2514
|
};
|
|
1943
2515
|
if (fs.existsSync(quarantine.path)) {
|
|
1944
2516
|
const existing = fs.lstatSync(quarantine.path, { bigint: true });
|
|
1945
2517
|
if (
|
|
1946
|
-
existing.isSymbolicLink()
|
|
1947
|
-
|
|
1948
|
-
existing.dev
|
|
1949
|
-
existing.ino
|
|
1950
|
-
existing.size
|
|
1951
|
-
existing.mtimeNs
|
|
1952
|
-
)
|
|
1953
|
-
|
|
1954
|
-
|
|
2518
|
+
!existing.isSymbolicLink() &&
|
|
2519
|
+
existing.isDirectory() &&
|
|
2520
|
+
existing.dev === BigInt(identity.dev) &&
|
|
2521
|
+
existing.ino === BigInt(identity.ino) &&
|
|
2522
|
+
existing.size === BigInt(identity.size) &&
|
|
2523
|
+
existing.mtimeNs === BigInt(identity.mtimeNs)
|
|
2524
|
+
) {
|
|
2525
|
+
assertPreparedTree(quarantine.path);
|
|
2526
|
+
}
|
|
2527
|
+
// A changed source pathname is independent retained authority. Do not replace
|
|
2528
|
+
// it with the detached original; retain both roots for recovery.
|
|
1955
2529
|
return;
|
|
1956
2530
|
}
|
|
1957
2531
|
assertPreparedTree(quarantine.detachedPath);
|
|
@@ -1965,7 +2539,9 @@ function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandida
|
|
|
1965
2539
|
if (!result.ok) throw new Error("durability_failed");
|
|
1966
2540
|
}
|
|
1967
2541
|
|
|
1968
|
-
function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot): void {
|
|
2542
|
+
function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot, cleanup?: SourceArtifactCleanup): void {
|
|
2543
|
+
if (cleanup && !cleanupAuthorityMatches(cleanup, path.dirname(detached.originalPath)))
|
|
2544
|
+
throw new Error("durability_failed");
|
|
1969
2545
|
const result = native.exactRestore(detached.detachedPath, detached.originalPath, {
|
|
1970
2546
|
...detached.identity,
|
|
1971
2547
|
directory: true,
|
|
@@ -2030,7 +2606,7 @@ async function copyArtifacts(
|
|
|
2030
2606
|
function migrationReceipt(
|
|
2031
2607
|
scope: ManagedScope,
|
|
2032
2608
|
lock: ManagedStorageLock,
|
|
2033
|
-
state: "prepared" | "published" | "committed",
|
|
2609
|
+
state: "prepared" | "artifact_detached" | "published" | "committed",
|
|
2034
2610
|
source: ManagedCandidate,
|
|
2035
2611
|
destination: ManagedCandidate | { path: string; sessionId: string; cwd: string },
|
|
2036
2612
|
manifest: readonly ArtifactManifestEntry[],
|
|
@@ -2039,7 +2615,9 @@ function migrationReceipt(
|
|
|
2039
2615
|
detachedPath: string;
|
|
2040
2616
|
identity: DetachedArtifactRoot["identity"];
|
|
2041
2617
|
tree: NativeDirectoryTreeSnapshot;
|
|
2618
|
+
role?: "detached_artifact_root";
|
|
2042
2619
|
},
|
|
2620
|
+
sourceArtifactCleanup?: SourceArtifactCleanup,
|
|
2043
2621
|
): Uint8Array {
|
|
2044
2622
|
lock.assertOwned();
|
|
2045
2623
|
const destinationRecord =
|
|
@@ -2057,12 +2635,20 @@ function migrationReceipt(
|
|
|
2057
2635
|
header: { id: destination.sessionId, cwd: destination.cwd },
|
|
2058
2636
|
};
|
|
2059
2637
|
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`,
|
|
2638
|
+
`${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 } : {}) }, (_key, value: unknown) => (typeof value === "bigint" ? value.toString() : value))}\n`,
|
|
2061
2639
|
);
|
|
2062
2640
|
}
|
|
2063
2641
|
|
|
2642
|
+
function detachedReceiptMatches(receipt: string, expected: Uint8Array): boolean {
|
|
2643
|
+
try {
|
|
2644
|
+
return captureManagedFileNoFollow(receipt).bytes.equals(expected);
|
|
2645
|
+
} catch {
|
|
2646
|
+
return false;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2064
2650
|
async function removeStagedReceipts(scope: ManagedScope, candidate: ManagedCandidate): Promise<void> {
|
|
2065
|
-
for (const state of ["prepared", "published"] as const) {
|
|
2651
|
+
for (const state of ["prepared", "detached", "published"] as const) {
|
|
2066
2652
|
const pathname = receiptPathFor(scope, candidate, state);
|
|
2067
2653
|
try {
|
|
2068
2654
|
const stat = fs.lstatSync(pathname);
|
|
@@ -2145,8 +2731,7 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2145
2731
|
if (!observedPending) continue;
|
|
2146
2732
|
if (
|
|
2147
2733
|
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2148
|
-
|
|
2149
|
-
!fs.existsSync(observedPending.plannedTranscriptPath)
|
|
2734
|
+
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2150
2735
|
) {
|
|
2151
2736
|
fsyncManagedParent(target.path);
|
|
2152
2737
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -2185,6 +2770,12 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2185
2770
|
? pending.plannedTranscriptPath
|
|
2186
2771
|
: undefined) ??
|
|
2187
2772
|
(fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
|
|
2773
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2774
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2775
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2776
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2777
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2778
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2188
2779
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2189
2780
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2190
2781
|
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
@@ -2201,6 +2792,14 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2201
2792
|
transcriptIdentity: target.identity,
|
|
2202
2793
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2203
2794
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2795
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
2796
|
+
|
|
2797
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
2798
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
2799
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
2800
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
2801
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
2802
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2204
2803
|
artifactsRemoved: true,
|
|
2205
2804
|
});
|
|
2206
2805
|
}
|
|
@@ -2221,10 +2820,34 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
2221
2820
|
deletion.phase === "transcript"
|
|
2222
2821
|
? deletion.detachedTranscriptPath
|
|
2223
2822
|
: active.detachedTranscriptPath,
|
|
2823
|
+
retainedArtifactsSuccessorPath:
|
|
2824
|
+
deletion.phase === "artifacts"
|
|
2825
|
+
? deletion.retainedSuccessorPath
|
|
2826
|
+
: active.retainedArtifactsSuccessorPath,
|
|
2827
|
+
retainedArtifactsPlaceholderPath:
|
|
2828
|
+
deletion.phase === "artifacts"
|
|
2829
|
+
? deletion.retainedPlaceholderPath
|
|
2830
|
+
: active.retainedArtifactsPlaceholderPath,
|
|
2831
|
+
retainedArtifactsUnknownPath:
|
|
2832
|
+
deletion.phase === "artifacts"
|
|
2833
|
+
? deletion.retainedUnknownPath
|
|
2834
|
+
: active.retainedArtifactsUnknownPath,
|
|
2835
|
+
retainedTranscriptSuccessorPath:
|
|
2836
|
+
deletion.phase === "transcript"
|
|
2837
|
+
? deletion.retainedSuccessorPath
|
|
2838
|
+
: active.retainedTranscriptSuccessorPath,
|
|
2839
|
+
retainedTranscriptPlaceholderPath:
|
|
2840
|
+
deletion.phase === "transcript"
|
|
2841
|
+
? deletion.retainedPlaceholderPath
|
|
2842
|
+
: active.retainedTranscriptPlaceholderPath,
|
|
2843
|
+
retainedTranscriptUnknownPath:
|
|
2844
|
+
deletion.phase === "transcript"
|
|
2845
|
+
? deletion.retainedUnknownPath
|
|
2846
|
+
: active.retainedTranscriptUnknownPath,
|
|
2224
2847
|
},
|
|
2225
2848
|
lock,
|
|
2226
2849
|
);
|
|
2227
|
-
|
|
2850
|
+
continue;
|
|
2228
2851
|
}
|
|
2229
2852
|
fsyncManagedParent(target.path);
|
|
2230
2853
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -2257,11 +2880,24 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
2257
2880
|
await reconcileManagedTombstones(scope);
|
|
2258
2881
|
return { kind: "resolved", scope };
|
|
2259
2882
|
} catch (error) {
|
|
2260
|
-
|
|
2883
|
+
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
2884
|
+
const message =
|
|
2885
|
+
publication?.classification ??
|
|
2886
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
2887
|
+
const code =
|
|
2888
|
+
message === "atomic_unavailable" ||
|
|
2889
|
+
message === "invalid_request" ||
|
|
2890
|
+
message === "durability_failed" ||
|
|
2891
|
+
message === "durability_not_provable"
|
|
2892
|
+
? message
|
|
2893
|
+
: "binding_invalid";
|
|
2261
2894
|
return {
|
|
2262
2895
|
kind: "error",
|
|
2263
|
-
code
|
|
2264
|
-
message
|
|
2896
|
+
code,
|
|
2897
|
+
message,
|
|
2898
|
+
...(publication
|
|
2899
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
2900
|
+
: { cause: { classification: code } }),
|
|
2265
2901
|
};
|
|
2266
2902
|
}
|
|
2267
2903
|
}
|
|
@@ -2300,7 +2936,8 @@ export async function openManagedCandidateForWrite(
|
|
|
2300
2936
|
if (prepared.kind === "error")
|
|
2301
2937
|
return {
|
|
2302
2938
|
kind: "error",
|
|
2303
|
-
code: prepared.code
|
|
2939
|
+
code: expectedFailure(new Error(prepared.code)),
|
|
2940
|
+
|
|
2304
2941
|
message: prepared.message,
|
|
2305
2942
|
};
|
|
2306
2943
|
current = revalidatePickerConsent(scope, candidate, expectedIdentity);
|
|
@@ -2319,11 +2956,15 @@ export async function openManagedCandidateForWrite(
|
|
|
2319
2956
|
const internal = managedInternalDirectory(scope);
|
|
2320
2957
|
let lock: ManagedStorageLock | undefined;
|
|
2321
2958
|
let detachedArtifacts: DetachedArtifactRoot | undefined;
|
|
2959
|
+
let sourceArtifactCleanup: SourceArtifactCleanup | undefined;
|
|
2960
|
+
|
|
2322
2961
|
try {
|
|
2323
2962
|
revalidatePickerConsent(scope, current, expectedIdentity);
|
|
2324
2963
|
lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation, scopeRoot(scope));
|
|
2325
2964
|
const heldLock = lock;
|
|
2965
|
+
|
|
2326
2966
|
const afterLock = revalidatePickerConsent(scope, current, expectedIdentity);
|
|
2967
|
+
|
|
2327
2968
|
const listing = listManagedCandidates(scope);
|
|
2328
2969
|
if (listing.kind === "error") return { kind: "error", code: "binding_invalid", message: listing.message };
|
|
2329
2970
|
const destination = path.join(scope.directoryPath, path.basename(afterLock.path));
|
|
@@ -2335,11 +2976,22 @@ export async function openManagedCandidateForWrite(
|
|
|
2335
2976
|
code: "destination_conflict",
|
|
2336
2977
|
message: "A distinct v2 transcript already owns this session id.",
|
|
2337
2978
|
};
|
|
2979
|
+
|
|
2338
2980
|
scopeRoot(scope);
|
|
2339
2981
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2340
2982
|
restorePreparedArtifactRoot(scope, afterLock);
|
|
2341
2983
|
scopeRoot(scope);
|
|
2342
|
-
|
|
2984
|
+
|
|
2985
|
+
let sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
|
|
2986
|
+
if (
|
|
2987
|
+
sourceSnapshot.identity.dev !== afterLock.identity.dev ||
|
|
2988
|
+
sourceSnapshot.identity.ino !== afterLock.identity.ino ||
|
|
2989
|
+
sourceSnapshot.identity.size !== afterLock.identity.size ||
|
|
2990
|
+
sourceSnapshot.identity.mtimeNs !== afterLock.identity.mtimeNs ||
|
|
2991
|
+
sourceSnapshot.identity.sha256 !== afterLock.identity.sha256 ||
|
|
2992
|
+
sourceSnapshot.bytes.byteLength !== afterLock.identity.size
|
|
2993
|
+
)
|
|
2994
|
+
throw new Error("source_changed");
|
|
2343
2995
|
let manifest: readonly ArtifactManifestEntry[] = [];
|
|
2344
2996
|
const artifactPlan = planArtifactRootForMigration(afterLock.path, operation);
|
|
2345
2997
|
const intendedDestination = { path: destination, sessionId: afterLock.sessionId, cwd: afterLock.cwd };
|
|
@@ -2361,6 +3013,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2361
3013
|
message: "The migration destination already exists without validated ownership.",
|
|
2362
3014
|
};
|
|
2363
3015
|
}
|
|
3016
|
+
|
|
2364
3017
|
const preparedReceipt = receiptPathFor(scope, afterLock, "prepared");
|
|
2365
3018
|
try {
|
|
2366
3019
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
@@ -2389,14 +3042,52 @@ export async function openManagedCandidateForWrite(
|
|
|
2389
3042
|
} catch (error) {
|
|
2390
3043
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2391
3044
|
}
|
|
3045
|
+
|
|
3046
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2392
3047
|
lock.assertOwned();
|
|
2393
3048
|
if (!preparedReceiptMatches(preparedReceipt, scope, afterLock, intendedDestination, artifactPlan))
|
|
2394
3049
|
throw new Error("durability_failed");
|
|
2395
3050
|
if (artifactPlan && fs.existsSync(artifactPlan.detachedPath)) throw new Error("destination_conflict");
|
|
3051
|
+
|
|
2396
3052
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3053
|
+
|
|
2397
3054
|
scopeRoot(scope);
|
|
2398
|
-
|
|
3055
|
+
|
|
3056
|
+
if (artifactPlan) {
|
|
3057
|
+
const detached = detachArtifactRootForMigration(artifactPlan);
|
|
3058
|
+
detachedArtifacts = detached.detached;
|
|
3059
|
+
sourceArtifactCleanup = detached.cleanup;
|
|
3060
|
+
const detachedReceipt = receiptPathFor(scope, afterLock, "detached");
|
|
3061
|
+
const detachedRecord = migrationReceipt(
|
|
3062
|
+
scope,
|
|
3063
|
+
lock,
|
|
3064
|
+
"artifact_detached",
|
|
3065
|
+
afterLock,
|
|
3066
|
+
intendedDestination,
|
|
3067
|
+
[],
|
|
3068
|
+
{
|
|
3069
|
+
path: detachedArtifacts.originalPath,
|
|
3070
|
+
detachedPath: detachedArtifacts.detachedPath,
|
|
3071
|
+
identity: detachedArtifacts.identity,
|
|
3072
|
+
tree: detachedArtifacts.tree,
|
|
3073
|
+
role: "detached_artifact_root",
|
|
3074
|
+
},
|
|
3075
|
+
sourceArtifactCleanup,
|
|
3076
|
+
);
|
|
3077
|
+
try {
|
|
3078
|
+
await publishManagedFileNoReplace(
|
|
3079
|
+
detachedReceipt,
|
|
3080
|
+
detachedRecord,
|
|
3081
|
+
assertPublicationConsent,
|
|
3082
|
+
scopeRoot(scope),
|
|
3083
|
+
);
|
|
3084
|
+
} catch (error) {
|
|
3085
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
3086
|
+
}
|
|
3087
|
+
if (!detachedReceiptMatches(detachedReceipt, detachedRecord)) throw new Error("durability_failed");
|
|
3088
|
+
}
|
|
2399
3089
|
manifest = detachedArtifacts ? artifactManifestFromSnapshot(detachedArtifacts.tree) : [];
|
|
3090
|
+
|
|
2400
3091
|
await copyArtifacts(
|
|
2401
3092
|
scope,
|
|
2402
3093
|
afterLock.path,
|
|
@@ -2407,6 +3098,22 @@ export async function openManagedCandidateForWrite(
|
|
|
2407
3098
|
expectedIdentity,
|
|
2408
3099
|
detachedArtifacts?.detachedPath,
|
|
2409
3100
|
);
|
|
3101
|
+
|
|
3102
|
+
if (!existing) {
|
|
3103
|
+
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
3104
|
+
lock.assertOwned();
|
|
3105
|
+
sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
|
|
3106
|
+
if (
|
|
3107
|
+
sourceSnapshot.identity.dev !== afterLock.identity.dev ||
|
|
3108
|
+
sourceSnapshot.identity.ino !== afterLock.identity.ino ||
|
|
3109
|
+
sourceSnapshot.identity.size !== afterLock.identity.size ||
|
|
3110
|
+
sourceSnapshot.identity.mtimeNs !== afterLock.identity.mtimeNs ||
|
|
3111
|
+
sourceSnapshot.identity.sha256 !== afterLock.identity.sha256 ||
|
|
3112
|
+
sourceSnapshot.bytes.byteLength !== afterLock.identity.size
|
|
3113
|
+
)
|
|
3114
|
+
throw new Error("source_changed");
|
|
3115
|
+
}
|
|
3116
|
+
|
|
2410
3117
|
if (!existing) {
|
|
2411
3118
|
try {
|
|
2412
3119
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
@@ -2416,6 +3123,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2416
3123
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2417
3124
|
}
|
|
2418
3125
|
}
|
|
3126
|
+
|
|
2419
3127
|
// Artifact files, directories, and the transcript must be durable before a receipt can grant shadow authority.
|
|
2420
3128
|
scopeRoot(scope);
|
|
2421
3129
|
if (manifest.length > 0) fsyncManagedArtifactTree(destination.slice(0, -6));
|
|
@@ -2429,12 +3137,14 @@ export async function openManagedCandidateForWrite(
|
|
|
2429
3137
|
!manifestMatches(destination, manifest)
|
|
2430
3138
|
)
|
|
2431
3139
|
throw new Error("durability_failed");
|
|
3140
|
+
|
|
2432
3141
|
if (detachedArtifacts) {
|
|
2433
3142
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2434
3143
|
scopeRoot(scope);
|
|
2435
|
-
restoreDetachedArtifactRoot(detachedArtifacts);
|
|
3144
|
+
restoreDetachedArtifactRoot(detachedArtifacts, sourceArtifactCleanup);
|
|
2436
3145
|
detachedArtifacts = undefined;
|
|
2437
3146
|
}
|
|
3147
|
+
|
|
2438
3148
|
const latest = validateCandidateForScope(scope, afterLock);
|
|
2439
3149
|
if (!latest || !sameCandidate(latest, afterLock) || !manifestMatches(afterLock.path, manifest))
|
|
2440
3150
|
return {
|
|
@@ -2442,6 +3152,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2442
3152
|
code: "source_changed",
|
|
2443
3153
|
message: "The legacy candidate or its artifacts changed before migration commit.",
|
|
2444
3154
|
};
|
|
3155
|
+
|
|
2445
3156
|
for (const state of ["published", "committed"] as const) {
|
|
2446
3157
|
const receipt = receiptPathFor(scope, afterLock, state);
|
|
2447
3158
|
try {
|
|
@@ -2449,7 +3160,24 @@ export async function openManagedCandidateForWrite(
|
|
|
2449
3160
|
lock.assertOwned();
|
|
2450
3161
|
await publishManagedFileNoReplace(
|
|
2451
3162
|
receipt,
|
|
2452
|
-
migrationReceipt(
|
|
3163
|
+
migrationReceipt(
|
|
3164
|
+
scope,
|
|
3165
|
+
lock,
|
|
3166
|
+
state,
|
|
3167
|
+
afterLock,
|
|
3168
|
+
migrated,
|
|
3169
|
+
manifest,
|
|
3170
|
+
artifactPlan
|
|
3171
|
+
? {
|
|
3172
|
+
path: artifactPlan.originalPath,
|
|
3173
|
+
detachedPath: artifactPlan.detachedPath,
|
|
3174
|
+
identity: artifactPlan.identity,
|
|
3175
|
+
tree: artifactPlan.tree,
|
|
3176
|
+
role: "detached_artifact_root",
|
|
3177
|
+
}
|
|
3178
|
+
: undefined,
|
|
3179
|
+
sourceArtifactCleanup,
|
|
3180
|
+
),
|
|
2453
3181
|
assertPublicationConsent,
|
|
2454
3182
|
scopeRoot(scope),
|
|
2455
3183
|
);
|
|
@@ -2457,6 +3185,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2457
3185
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2458
3186
|
}
|
|
2459
3187
|
}
|
|
3188
|
+
|
|
2460
3189
|
const receipt = receiptPathFor(scope, afterLock);
|
|
2461
3190
|
lock.assertOwned();
|
|
2462
3191
|
if (!receiptMatches(receipt, afterLock, migrated, scope))
|
|
@@ -2465,6 +3194,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2465
3194
|
code: "durability_failed",
|
|
2466
3195
|
message: "The migration receipt does not bind the copied v2 transcript and artifacts.",
|
|
2467
3196
|
};
|
|
3197
|
+
|
|
2468
3198
|
revalidatePickerConsent(scope, afterLock, expectedIdentity);
|
|
2469
3199
|
scopeRoot(scope);
|
|
2470
3200
|
await removeStagedReceipts(scope, afterLock);
|
|
@@ -2476,7 +3206,7 @@ export async function openManagedCandidateForWrite(
|
|
|
2476
3206
|
};
|
|
2477
3207
|
} catch (error) {
|
|
2478
3208
|
try {
|
|
2479
|
-
if (detachedArtifacts) restoreDetachedArtifactRoot(detachedArtifacts);
|
|
3209
|
+
if (detachedArtifacts) restoreDetachedArtifactRoot(detachedArtifacts, sourceArtifactCleanup);
|
|
2480
3210
|
} catch {
|
|
2481
3211
|
return {
|
|
2482
3212
|
kind: "error",
|
|
@@ -2500,7 +3230,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
2500
3230
|
if (prepared.kind === "error")
|
|
2501
3231
|
return {
|
|
2502
3232
|
kind: "error",
|
|
2503
|
-
code: prepared.code
|
|
3233
|
+
code: expectedFailure(new Error(prepared.code)),
|
|
2504
3234
|
message: prepared.message,
|
|
2505
3235
|
};
|
|
2506
3236
|
const current = validateCandidateForScope(scope, candidate);
|
|
@@ -2560,8 +3290,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
2560
3290
|
if (!observedPending) continue;
|
|
2561
3291
|
if (
|
|
2562
3292
|
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2563
|
-
|
|
2564
|
-
!fs.existsSync(observedPending.plannedTranscriptPath)
|
|
3293
|
+
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2565
3294
|
) {
|
|
2566
3295
|
fsyncManagedParent(target.path);
|
|
2567
3296
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
@@ -2596,6 +3325,12 @@ export async function deleteManagedSessionCandidate(
|
|
|
2596
3325
|
active.detachedTranscriptPath ??
|
|
2597
3326
|
observedPending?.detachedTranscriptPath ??
|
|
2598
3327
|
(pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
|
|
3328
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3329
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3330
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3331
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3332
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3333
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2599
3334
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2600
3335
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
2601
3336
|
...(cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
@@ -2612,6 +3347,13 @@ export async function deleteManagedSessionCandidate(
|
|
|
2612
3347
|
transcriptIdentity: target.identity,
|
|
2613
3348
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
2614
3349
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3350
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3351
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3352
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3353
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3354
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3355
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3356
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
2615
3357
|
artifactsRemoved: true,
|
|
2616
3358
|
});
|
|
2617
3359
|
}
|
|
@@ -2627,23 +3369,51 @@ export async function deleteManagedSessionCandidate(
|
|
|
2627
3369
|
)
|
|
2628
3370
|
throw new Error("durability_failed");
|
|
2629
3371
|
const retry = nextCleanupReceipt(target, active);
|
|
2630
|
-
await publishCleanupPending(
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
3372
|
+
await publishCleanupPending(scope, tombstone, cleanupPendingEvidence(retry, active, deletion), lock);
|
|
3373
|
+
if (deletion.phase === "artifacts") {
|
|
3374
|
+
// A typed retained artifact root proves durable canonical absence; advance
|
|
3375
|
+
// to the transcript phase instead of stalling on the retained evidence.
|
|
3376
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
3377
|
+
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
3378
|
+
sessionsRoot: scope.sessionsRoot,
|
|
3379
|
+
transcriptPath: target.path,
|
|
3380
|
+
sessionId: target.sessionId,
|
|
3381
|
+
cwd: target.cwd,
|
|
3382
|
+
transcriptIdentity: target.identity,
|
|
3383
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3384
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3385
|
+
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3386
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3387
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3388
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3389
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3390
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3391
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
3392
|
+
artifactsRemoved: true,
|
|
3393
|
+
});
|
|
3394
|
+
if (deletion.kind === "cleanup_pending") {
|
|
3395
|
+
const followup = nextCleanupReceipt(target, active);
|
|
3396
|
+
await publishCleanupPending(
|
|
3397
|
+
scope,
|
|
3398
|
+
tombstone,
|
|
3399
|
+
cleanupPendingEvidence(followup, active, deletion),
|
|
3400
|
+
lock,
|
|
3401
|
+
);
|
|
3402
|
+
if (
|
|
3403
|
+
deletion.phase !== "transcript" ||
|
|
3404
|
+
deletion.detachedTranscriptPath !== active.plannedTranscriptPath
|
|
3405
|
+
)
|
|
3406
|
+
return {
|
|
3407
|
+
kind: "cleanup_pending",
|
|
3408
|
+
tombstonePath: tombstone,
|
|
3409
|
+
phase: deletion.phase,
|
|
3410
|
+
message:
|
|
3411
|
+
"Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
// A typed retained transcript quarantine is durable canonical-absence
|
|
3416
|
+
// evidence — never a terminal byte-deletion claim; complete the receipt.
|
|
2647
3417
|
}
|
|
2648
3418
|
fsyncManagedParent(target.path);
|
|
2649
3419
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|