@gajae-code/coding-agent 0.12.7 → 0.12.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 +19 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +31 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +464 -50
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +431 -130
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +10 -7
- package/src/session/agent-session.ts +44 -24
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +382 -45
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/telegram-send.ts +4 -2
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
verifyOwnerOnlyPathSecurity,
|
|
9
9
|
verifyOwnerOnlyPathSecurityExpected,
|
|
10
10
|
} from "@gajae-code/natives";
|
|
11
|
-
import { logger, pathIsWithin } from "@gajae-code/utils";
|
|
11
|
+
import { hasFsCode, logger, pathIsWithin } from "@gajae-code/utils";
|
|
12
12
|
import type { ResumeSessionIdentity } from "../session-manager";
|
|
13
13
|
import {
|
|
14
14
|
FileSessionStorage,
|
|
15
15
|
type NativeDirectoryTreeSnapshot,
|
|
16
16
|
type SessionStorageFileIdentity,
|
|
17
17
|
type VerifiedSessionDeleteResult,
|
|
18
|
+
type VerifiedSessionDeleteTarget,
|
|
18
19
|
} from "../session-storage";
|
|
19
20
|
import {
|
|
20
21
|
acquireManagedLock,
|
|
@@ -127,7 +128,7 @@ function configuredRootPath(scope: ManagedScope): string {
|
|
|
127
128
|
const canonical = fs.realpathSync.native(candidate);
|
|
128
129
|
return suffix.length === 0 ? canonical : path.join(canonical, ...suffix);
|
|
129
130
|
} catch (error) {
|
|
130
|
-
if ((error
|
|
131
|
+
if (!hasFsCode(error, "ENOENT")) throw error;
|
|
131
132
|
const parent = path.dirname(candidate);
|
|
132
133
|
if (parent === candidate) throw new Error("Configured managed root is unavailable.");
|
|
133
134
|
suffix.unshift(path.basename(candidate));
|
|
@@ -163,6 +164,7 @@ export type ManagedScopeErrorCode =
|
|
|
163
164
|
| "sessions_root_unavailable"
|
|
164
165
|
| "binding_conflict"
|
|
165
166
|
| "binding_invalid"
|
|
167
|
+
| "migration_busy"
|
|
166
168
|
| "atomic_unavailable"
|
|
167
169
|
| "invalid_request"
|
|
168
170
|
| "durability_failed"
|
|
@@ -177,6 +179,24 @@ export type ManagedScopeResolution =
|
|
|
177
179
|
cause?: { readonly classification: string; readonly diagnostic?: string };
|
|
178
180
|
};
|
|
179
181
|
|
|
182
|
+
function managedScopeFailureCause(error: unknown): { readonly classification: string } {
|
|
183
|
+
return { classification: managedSecurityFailureClassification(error) ?? "binding_invalid" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const managedScopeFailureCodes = new Set([
|
|
187
|
+
"atomic_unavailable",
|
|
188
|
+
"invalid_request",
|
|
189
|
+
"durability_failed",
|
|
190
|
+
"durability_not_provable",
|
|
191
|
+
"migration_busy",
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
function managedScopeFailureMessage(error: unknown, fallback: string): string {
|
|
195
|
+
const classification = managedSecurityFailureClassification(error);
|
|
196
|
+
if (classification) return classification;
|
|
197
|
+
return error instanceof Error && managedScopeFailureCodes.has(error.message) ? error.message : fallback;
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
export interface ManagedCandidate {
|
|
181
201
|
sessionId: string;
|
|
182
202
|
path: string;
|
|
@@ -222,6 +242,38 @@ export type ManagedDeleteCandidateResult =
|
|
|
222
242
|
| { kind: "cleanup_pending"; tombstonePath: string; phase: "artifacts" | "transcript"; message: string }
|
|
223
243
|
| { kind: "error"; code: ManagedOpenFailure; message: string };
|
|
224
244
|
|
|
245
|
+
export interface ManagedVerifiedDeleteTestEvent {
|
|
246
|
+
readonly flow: "direct" | "reconcile";
|
|
247
|
+
readonly stage: "initial" | "artifact-finalization" | "transcript-after-artifacts-removed";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface ManagedLockReleaseTestEvent {
|
|
251
|
+
readonly path: string;
|
|
252
|
+
readonly attemptId: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Test-only ordering seams for verified deletion and managed-lock release. */
|
|
256
|
+
export const ManagedSessionScopeTestHooks: {
|
|
257
|
+
beforeVerifiedDelete?: (event: ManagedVerifiedDeleteTestEvent) => void | Promise<void>;
|
|
258
|
+
beforeManagedLockRelease?: (event: ManagedLockReleaseTestEvent) => void | Promise<void>;
|
|
259
|
+
} = {};
|
|
260
|
+
|
|
261
|
+
async function deleteSessionVerifiedWithFence(
|
|
262
|
+
flow: ManagedVerifiedDeleteTestEvent["flow"],
|
|
263
|
+
stage: ManagedVerifiedDeleteTestEvent["stage"],
|
|
264
|
+
lock: ManagedStorageLock,
|
|
265
|
+
target: VerifiedSessionDeleteTarget,
|
|
266
|
+
verifyAuthority?: () => void,
|
|
267
|
+
): Promise<VerifiedSessionDeleteResult> {
|
|
268
|
+
lock.assertOwned();
|
|
269
|
+
verifyAuthority?.();
|
|
270
|
+
const hook = ManagedSessionScopeTestHooks.beforeVerifiedDelete;
|
|
271
|
+
if (hook) await hook({ flow, stage });
|
|
272
|
+
lock.assertOwned();
|
|
273
|
+
verifyAuthority?.();
|
|
274
|
+
return new FileSessionStorage().deleteSessionVerified(target);
|
|
275
|
+
}
|
|
276
|
+
|
|
225
277
|
type NativeIdentity =
|
|
226
278
|
| { ok: true; platform: "posix" | "win32"; canonicalPath: string }
|
|
227
279
|
| { ok: false; code: NativeIdentityFailureCode };
|
|
@@ -277,7 +329,7 @@ function identityFor(cwd: string): NativeIdentity {
|
|
|
277
329
|
function verifyExistingManagedScopeDirectory(pathname: string) {
|
|
278
330
|
if (process.platform !== "win32") return verifyOwnerOnlyPathSecurity(pathname, "directory");
|
|
279
331
|
const expected = fs.lstatSync(pathname, { bigint: true });
|
|
280
|
-
if (!expected.isDirectory() || expected.isSymbolicLink()) throw new Error("
|
|
332
|
+
if (!expected.isDirectory() || expected.isSymbolicLink()) throw new Error("reparse_point");
|
|
281
333
|
const verified = verifyOwnerOnlyPathSecurityExpected(pathname, "directory", expected.dev, expected.ino);
|
|
282
334
|
const current = fs.lstatSync(pathname, { bigint: true });
|
|
283
335
|
if (
|
|
@@ -286,7 +338,7 @@ function verifyExistingManagedScopeDirectory(pathname: string) {
|
|
|
286
338
|
current.dev !== expected.dev ||
|
|
287
339
|
current.ino !== expected.ino
|
|
288
340
|
)
|
|
289
|
-
throw new Error("
|
|
341
|
+
throw new Error("identity_mismatch");
|
|
290
342
|
return verified;
|
|
291
343
|
}
|
|
292
344
|
|
|
@@ -405,8 +457,18 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
|
|
|
405
457
|
try {
|
|
406
458
|
raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
|
|
407
459
|
} catch (error) {
|
|
408
|
-
if ((error
|
|
409
|
-
|
|
460
|
+
if (hasFsCode(error, "ENOENT")) return undefined;
|
|
461
|
+
const classification = hasFsCode(error, "EACCES")
|
|
462
|
+
? "EACCES"
|
|
463
|
+
: hasFsCode(error, "EPERM")
|
|
464
|
+
? "EPERM"
|
|
465
|
+
: "binding_invalid";
|
|
466
|
+
return {
|
|
467
|
+
kind: "error",
|
|
468
|
+
code: "binding_invalid",
|
|
469
|
+
message: "The managed scope binding is invalid JSON.",
|
|
470
|
+
cause: { classification },
|
|
471
|
+
};
|
|
410
472
|
}
|
|
411
473
|
return validateBindingRaw(scope, raw);
|
|
412
474
|
}
|
|
@@ -429,10 +491,11 @@ function resolveManagedScopeInternal(
|
|
|
429
491
|
kind: "error",
|
|
430
492
|
code: "sessions_root_unavailable",
|
|
431
493
|
message: "The sessions root is not a safe directory.",
|
|
494
|
+
cause: { classification: "reparse_point" },
|
|
432
495
|
};
|
|
433
496
|
}
|
|
434
497
|
} catch (error) {
|
|
435
|
-
if ((error
|
|
498
|
+
if (!hasFsCode(error, "ENOENT")) {
|
|
436
499
|
return {
|
|
437
500
|
kind: "error",
|
|
438
501
|
code: "sessions_root_unavailable",
|
|
@@ -462,10 +525,11 @@ function resolveManagedScopeInternal(
|
|
|
462
525
|
kind: "error",
|
|
463
526
|
code: "sessions_root_unavailable",
|
|
464
527
|
message: "The sessions root is not a safe directory.",
|
|
528
|
+
cause: { classification: "reparse_point" },
|
|
465
529
|
};
|
|
466
530
|
}
|
|
467
531
|
} catch (error) {
|
|
468
|
-
if ((error
|
|
532
|
+
if (!hasFsCode(error, "ENOENT")) {
|
|
469
533
|
return {
|
|
470
534
|
kind: "error",
|
|
471
535
|
code: "sessions_root_unavailable",
|
|
@@ -476,7 +540,12 @@ function resolveManagedScopeInternal(
|
|
|
476
540
|
try {
|
|
477
541
|
const directory = fs.lstatSync(scope.directoryPath);
|
|
478
542
|
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
479
|
-
return {
|
|
543
|
+
return {
|
|
544
|
+
kind: "error",
|
|
545
|
+
code: "binding_invalid",
|
|
546
|
+
message: "The managed scope path is not a safe directory.",
|
|
547
|
+
cause: { classification: "reparse_point" },
|
|
548
|
+
};
|
|
480
549
|
}
|
|
481
550
|
const security = validateNativeSecurityResult(
|
|
482
551
|
verifyExistingManagedScopeDirectory(scope.directoryPath),
|
|
@@ -491,8 +560,12 @@ function resolveManagedScopeInternal(
|
|
|
491
560
|
};
|
|
492
561
|
}
|
|
493
562
|
} catch (error) {
|
|
494
|
-
if ((error
|
|
495
|
-
return {
|
|
563
|
+
if (!hasFsCode(error, "ENOENT")) {
|
|
564
|
+
return {
|
|
565
|
+
kind: "error",
|
|
566
|
+
code: "binding_invalid",
|
|
567
|
+
message: "The managed scope path could not be inspected.",
|
|
568
|
+
};
|
|
496
569
|
}
|
|
497
570
|
}
|
|
498
571
|
return validateExistingBinding(scope) ?? { kind: "resolved", scope };
|
|
@@ -557,7 +630,7 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
557
630
|
try {
|
|
558
631
|
descriptor = fs.openSync(parent, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
|
|
559
632
|
} catch (error) {
|
|
560
|
-
if ((error
|
|
633
|
+
if (hasFsCode(error, "ENOENT") && path.dirname(parent) !== parent) {
|
|
561
634
|
parent = path.dirname(parent);
|
|
562
635
|
continue;
|
|
563
636
|
}
|
|
@@ -692,7 +765,6 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
|
|
|
692
765
|
named.isSymbolicLink() ||
|
|
693
766
|
named.dev !== snapshot.identity.dev ||
|
|
694
767
|
named.ino !== snapshot.identity.ino ||
|
|
695
|
-
named.nlink !== 1n ||
|
|
696
768
|
Number(named.size) !== snapshot.identity.size ||
|
|
697
769
|
named.mtimeNs !== snapshot.identity.mtimeNs
|
|
698
770
|
)
|
|
@@ -707,7 +779,7 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
|
|
|
707
779
|
canonicalPath: path.resolve(filePath),
|
|
708
780
|
sessionId: header.id,
|
|
709
781
|
...snapshot.identity,
|
|
710
|
-
nlink:
|
|
782
|
+
nlink: snapshot.identity.nlink,
|
|
711
783
|
mtimeMs: Number(named.mtimeMs),
|
|
712
784
|
sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
713
785
|
},
|
|
@@ -781,7 +853,7 @@ export async function ensureManagedScope(
|
|
|
781
853
|
try {
|
|
782
854
|
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
|
|
783
855
|
} catch (error) {
|
|
784
|
-
if ((error
|
|
856
|
+
if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
|
|
785
857
|
bindingCollision = true;
|
|
786
858
|
}
|
|
787
859
|
const validated = validateExistingBinding(scope);
|
|
@@ -789,15 +861,14 @@ export async function ensureManagedScope(
|
|
|
789
861
|
if (bindingCollision) fsyncCanonicalBinding(bindingPath, binding);
|
|
790
862
|
|
|
791
863
|
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
792
|
-
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
793
|
-
throw new Error("Managed session directory changed");
|
|
864
|
+
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink()) throw new Error("reparse_point");
|
|
794
865
|
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
795
866
|
return { kind: "resolved", scope };
|
|
796
867
|
} catch (error) {
|
|
797
868
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
798
869
|
const message =
|
|
799
870
|
publication?.classification ??
|
|
800
|
-
(error
|
|
871
|
+
managedScopeFailureMessage(error, "The managed scope could not be initialized.");
|
|
801
872
|
const code =
|
|
802
873
|
message === "atomic_unavailable" ||
|
|
803
874
|
message === "invalid_request" ||
|
|
@@ -805,14 +876,13 @@ export async function ensureManagedScope(
|
|
|
805
876
|
message === "durability_not_provable"
|
|
806
877
|
? message
|
|
807
878
|
: "binding_invalid";
|
|
808
|
-
|
|
809
879
|
return {
|
|
810
880
|
kind: "error",
|
|
811
881
|
code,
|
|
812
882
|
message,
|
|
813
|
-
|
|
814
|
-
? {
|
|
815
|
-
:
|
|
883
|
+
cause: publication
|
|
884
|
+
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
885
|
+
: managedScopeFailureCause(error),
|
|
816
886
|
};
|
|
817
887
|
}
|
|
818
888
|
}
|
|
@@ -948,7 +1018,7 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
948
1018
|
try {
|
|
949
1019
|
store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
|
|
950
1020
|
} catch (error) {
|
|
951
|
-
if ((error
|
|
1021
|
+
if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
|
|
952
1022
|
}
|
|
953
1023
|
stage = "binding_read";
|
|
954
1024
|
const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
@@ -986,8 +1056,7 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
986
1056
|
} catch (error) {
|
|
987
1057
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
988
1058
|
const message =
|
|
989
|
-
publication?.classification ??
|
|
990
|
-
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
1059
|
+
publication?.classification ?? managedScopeFailureMessage(error, "Managed write protocol setup failed.");
|
|
991
1060
|
const code =
|
|
992
1061
|
message === "atomic_unavailable" ||
|
|
993
1062
|
message === "invalid_request" ||
|
|
@@ -995,14 +1064,14 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
995
1064
|
message === "durability_not_provable"
|
|
996
1065
|
? message
|
|
997
1066
|
: "binding_invalid";
|
|
998
|
-
|
|
1067
|
+
|
|
999
1068
|
return {
|
|
1000
1069
|
kind: "error",
|
|
1001
1070
|
code,
|
|
1002
1071
|
message,
|
|
1003
1072
|
cause: publication
|
|
1004
1073
|
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
1005
|
-
: {
|
|
1074
|
+
: { ...managedScopeFailureCause(error), diagnostic: `prepare:${stage}` },
|
|
1006
1075
|
};
|
|
1007
1076
|
}
|
|
1008
1077
|
}
|
|
@@ -1140,7 +1209,6 @@ function sameCandidate(left: ManagedCandidate, right: ManagedCandidate): boolean
|
|
|
1140
1209
|
left.path === right.path &&
|
|
1141
1210
|
left.identity.dev === right.identity.dev &&
|
|
1142
1211
|
left.identity.ino === right.identity.ino &&
|
|
1143
|
-
left.identity.nlink === right.identity.nlink &&
|
|
1144
1212
|
left.identity.size === right.identity.size &&
|
|
1145
1213
|
left.identity.mtimeNs === right.identity.mtimeNs &&
|
|
1146
1214
|
left.identity.sha256 === right.identity.sha256
|
|
@@ -1153,7 +1221,6 @@ function matchesExpectedResumeIdentity(candidate: ManagedCandidate, expected: Re
|
|
|
1153
1221
|
candidate.identity.sessionId === expected.sessionId &&
|
|
1154
1222
|
candidate.identity.dev === expected.dev &&
|
|
1155
1223
|
candidate.identity.ino === expected.ino &&
|
|
1156
|
-
candidate.identity.nlink === expected.nlink &&
|
|
1157
1224
|
candidate.identity.size === expected.size &&
|
|
1158
1225
|
candidate.identity.mtimeMs === expected.mtimeMs &&
|
|
1159
1226
|
candidate.identity.mtimeNs === expected.mtimeNs &&
|
|
@@ -1218,7 +1285,14 @@ function receiptMatches(
|
|
|
1218
1285
|
state?: unknown;
|
|
1219
1286
|
role?: unknown;
|
|
1220
1287
|
retainedPath?: unknown;
|
|
1221
|
-
identity?: {
|
|
1288
|
+
identity?: {
|
|
1289
|
+
dev?: unknown;
|
|
1290
|
+
ino?: unknown;
|
|
1291
|
+
size?: unknown;
|
|
1292
|
+
mtimeNs?: unknown;
|
|
1293
|
+
parentDev?: unknown;
|
|
1294
|
+
parentIno?: unknown;
|
|
1295
|
+
};
|
|
1222
1296
|
tree?: unknown;
|
|
1223
1297
|
};
|
|
1224
1298
|
};
|
|
@@ -1275,6 +1349,8 @@ function receiptMatches(
|
|
|
1275
1349
|
cleanupIdentity.ino !== undefined &&
|
|
1276
1350
|
cleanupIdentity.size !== undefined &&
|
|
1277
1351
|
cleanupIdentity.mtimeNs !== undefined &&
|
|
1352
|
+
cleanupIdentity.parentDev !== undefined &&
|
|
1353
|
+
cleanupIdentity.parentIno !== undefined &&
|
|
1278
1354
|
!!cleanupTree &&
|
|
1279
1355
|
cleanupAuthorityMatches(
|
|
1280
1356
|
{
|
|
@@ -1286,6 +1362,8 @@ function receiptMatches(
|
|
|
1286
1362
|
ino: BigInt(String(cleanupIdentity.ino)),
|
|
1287
1363
|
size: BigInt(String(cleanupIdentity.size)),
|
|
1288
1364
|
mtimeNs: BigInt(String(cleanupIdentity.mtimeNs)),
|
|
1365
|
+
parentDev: BigInt(String(cleanupIdentity.parentDev)),
|
|
1366
|
+
parentIno: BigInt(String(cleanupIdentity.parentIno)),
|
|
1289
1367
|
},
|
|
1290
1368
|
tree: cleanupTree,
|
|
1291
1369
|
},
|
|
@@ -1368,27 +1446,7 @@ function preparedReceiptMatches(
|
|
|
1368
1446
|
}
|
|
1369
1447
|
}
|
|
1370
1448
|
|
|
1371
|
-
type RetiredTarget = ManagedCandidate
|
|
1372
|
-
|
|
1373
|
-
function authorizeRetiredTarget(scope: ManagedScope, candidate: ManagedCandidate): RetiredTarget | undefined {
|
|
1374
|
-
try {
|
|
1375
|
-
const before = fs.lstatSync(path.dirname(candidate.path), { bigint: true });
|
|
1376
|
-
if (!before.isDirectory()) return undefined;
|
|
1377
|
-
const verified = validateCandidateForScope(scope, candidate);
|
|
1378
|
-
const after = fs.lstatSync(path.dirname(candidate.path), { bigint: true });
|
|
1379
|
-
if (
|
|
1380
|
-
!verified ||
|
|
1381
|
-
!sameCandidate(verified, candidate) ||
|
|
1382
|
-
!after.isDirectory() ||
|
|
1383
|
-
after.dev !== before.dev ||
|
|
1384
|
-
after.ino !== before.ino
|
|
1385
|
-
)
|
|
1386
|
-
return undefined;
|
|
1387
|
-
return { ...candidate, transcriptParentIdentity: { dev: before.dev, ino: before.ino } };
|
|
1388
|
-
} catch {
|
|
1389
|
-
return undefined;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1449
|
+
type RetiredTarget = ManagedCandidate;
|
|
1392
1450
|
|
|
1393
1451
|
function retiredTargets(scope: ManagedScope, pathname: string): readonly RetiredTarget[] | undefined {
|
|
1394
1452
|
try {
|
|
@@ -1409,14 +1467,6 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1409
1467
|
const identity = item.identity;
|
|
1410
1468
|
if (!identity || typeof identity !== "object" || Array.isArray(identity)) return undefined;
|
|
1411
1469
|
const fields = identity as Record<string, unknown>;
|
|
1412
|
-
const transcriptParentIdentity = item.transcriptParentIdentity;
|
|
1413
|
-
if (
|
|
1414
|
-
!transcriptParentIdentity ||
|
|
1415
|
-
typeof transcriptParentIdentity !== "object" ||
|
|
1416
|
-
Array.isArray(transcriptParentIdentity)
|
|
1417
|
-
)
|
|
1418
|
-
return undefined;
|
|
1419
|
-
const parentFields = transcriptParentIdentity as Record<string, unknown>;
|
|
1420
1470
|
if (
|
|
1421
1471
|
typeof item.path !== "string" ||
|
|
1422
1472
|
typeof item.sessionId !== "string" ||
|
|
@@ -1426,13 +1476,10 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1426
1476
|
typeof fields.canonicalPath !== "string" ||
|
|
1427
1477
|
typeof fields.dev !== "string" ||
|
|
1428
1478
|
typeof fields.ino !== "string" ||
|
|
1429
|
-
typeof fields.nlink !== "string" ||
|
|
1430
1479
|
typeof fields.size !== "number" ||
|
|
1431
1480
|
typeof fields.mtimeMs !== "number" ||
|
|
1432
1481
|
typeof fields.mtimeNs !== "string" ||
|
|
1433
|
-
typeof fields.sha256 !== "string"
|
|
1434
|
-
typeof parentFields.dev !== "string" ||
|
|
1435
|
-
typeof parentFields.ino !== "string"
|
|
1482
|
+
typeof fields.sha256 !== "string"
|
|
1436
1483
|
)
|
|
1437
1484
|
return undefined;
|
|
1438
1485
|
const provenance =
|
|
@@ -1451,17 +1498,12 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1451
1498
|
canonicalPath: fields.canonicalPath,
|
|
1452
1499
|
dev: BigInt(fields.dev),
|
|
1453
1500
|
ino: BigInt(fields.ino),
|
|
1454
|
-
nlink: BigInt(fields.nlink as string),
|
|
1455
1501
|
size: fields.size,
|
|
1456
1502
|
mtimeMs: fields.mtimeMs,
|
|
1457
1503
|
mtimeNs: BigInt(fields.mtimeNs),
|
|
1458
1504
|
sha256: fields.sha256,
|
|
1459
1505
|
sessionId: item.sessionId,
|
|
1460
1506
|
},
|
|
1461
|
-
transcriptParentIdentity: {
|
|
1462
|
-
dev: BigInt(parentFields.dev as string),
|
|
1463
|
-
ino: BigInt(parentFields.ino as string),
|
|
1464
|
-
},
|
|
1465
1507
|
});
|
|
1466
1508
|
}
|
|
1467
1509
|
return targets;
|
|
@@ -1483,13 +1525,19 @@ type RetainedArtifactsRootReceipt = {
|
|
|
1483
1525
|
};
|
|
1484
1526
|
|
|
1485
1527
|
function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
|
|
1486
|
-
const pathname = deterministicRemovalRoot(receipt.plannedArtifactsPath);
|
|
1528
|
+
const pathname = receipt.detachedArtifactsPath ?? deterministicRemovalRoot(receipt.plannedArtifactsPath);
|
|
1487
1529
|
if (!fs.existsSync(pathname)) return undefined;
|
|
1488
|
-
if (!receipt.
|
|
1530
|
+
if (!isQuarantinePath(receipt.target, pathname) || !receipt.expectedArtifactsIdentity)
|
|
1531
|
+
throw new Error("durability_failed");
|
|
1489
1532
|
const identity = artifactIdentityAt(pathname);
|
|
1490
|
-
if (
|
|
1533
|
+
if (
|
|
1534
|
+
!identity ||
|
|
1535
|
+
identity.dev !== receipt.expectedArtifactsIdentity.dev ||
|
|
1536
|
+
identity.ino !== receipt.expectedArtifactsIdentity.ino
|
|
1537
|
+
)
|
|
1491
1538
|
throw new Error("durability_failed");
|
|
1492
1539
|
const tree = snapshotArtifactTree(pathname);
|
|
1540
|
+
if (!artifactTreePayloadAbsent(tree)) throw new Error("durability_failed");
|
|
1493
1541
|
return { path: pathname, identity, tree };
|
|
1494
1542
|
}
|
|
1495
1543
|
|
|
@@ -1500,13 +1548,21 @@ function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean
|
|
|
1500
1548
|
const retained = record.retainedArtifactsRoot as Record<string, unknown>;
|
|
1501
1549
|
const identity = retained.identity;
|
|
1502
1550
|
const tree = artifactTreeSnapshot(retained.tree);
|
|
1551
|
+
const expectedRetainedPath =
|
|
1552
|
+
typeof record.detachedArtifactsPath === "string"
|
|
1553
|
+
? record.detachedArtifactsPath
|
|
1554
|
+
: typeof record.plannedArtifactsPath === "string"
|
|
1555
|
+
? deterministicRemovalRoot(record.plannedArtifactsPath)
|
|
1556
|
+
: undefined;
|
|
1503
1557
|
if (
|
|
1504
|
-
|
|
1505
|
-
retained.path !==
|
|
1558
|
+
!expectedRetainedPath ||
|
|
1559
|
+
retained.path !== expectedRetainedPath ||
|
|
1560
|
+
record.artifactsPayloadDurable !== true ||
|
|
1506
1561
|
!identity ||
|
|
1507
1562
|
typeof identity !== "object" ||
|
|
1508
1563
|
Array.isArray(identity) ||
|
|
1509
|
-
!tree
|
|
1564
|
+
!tree ||
|
|
1565
|
+
!artifactTreePayloadAbsent(tree)
|
|
1510
1566
|
)
|
|
1511
1567
|
throw new Error("durability_failed");
|
|
1512
1568
|
const artifactIdentity = identity as Record<string, unknown>;
|
|
@@ -1522,14 +1578,9 @@ function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean
|
|
|
1522
1578
|
const observed = artifactIdentityAt(retained.path);
|
|
1523
1579
|
if (
|
|
1524
1580
|
!observed ||
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
size: artifactIdentity.size,
|
|
1529
|
-
mtimeNs: BigInt(artifactIdentity.mtimeNs),
|
|
1530
|
-
sha256: artifactIdentity.sha256,
|
|
1531
|
-
}) ||
|
|
1532
|
-
JSON.stringify(snapshotArtifactTree(retained.path)) !== JSON.stringify(tree)
|
|
1581
|
+
observed.dev !== BigInt(artifactIdentity.dev) ||
|
|
1582
|
+
observed.ino !== BigInt(artifactIdentity.ino) ||
|
|
1583
|
+
!artifactTreePayloadAbsent(snapshotArtifactTree(retained.path))
|
|
1533
1584
|
)
|
|
1534
1585
|
throw new Error("durability_failed");
|
|
1535
1586
|
return true;
|
|
@@ -1538,12 +1589,13 @@ function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean
|
|
|
1538
1589
|
type CleanupReceipt = {
|
|
1539
1590
|
attempt: number;
|
|
1540
1591
|
target: RetiredTarget;
|
|
1541
|
-
transcriptParentIdentity: { dev: bigint; ino: bigint };
|
|
1542
1592
|
expectedArtifactsIdentity?: SessionStorageFileIdentity;
|
|
1543
|
-
artifactsAbsentAtAuthorization?: true;
|
|
1544
1593
|
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
1594
|
+
artifactsPayloadDurable?: true;
|
|
1595
|
+
artifactsRemovedAttempt?: number;
|
|
1545
1596
|
detachedArtifactsPath?: string;
|
|
1546
1597
|
detachedTranscriptPath?: string;
|
|
1598
|
+
transcriptPayloadDurable?: true;
|
|
1547
1599
|
retainedArtifactsSuccessorPath?: string;
|
|
1548
1600
|
retainedArtifactsPlaceholderPath?: string;
|
|
1549
1601
|
retainedArtifactsUnknownPath?: string;
|
|
@@ -1580,15 +1632,15 @@ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: Cleanup
|
|
|
1580
1632
|
cwd: receipt.target.cwd,
|
|
1581
1633
|
identity: receipt.target.identity,
|
|
1582
1634
|
},
|
|
1583
|
-
transcriptParentIdentity: {
|
|
1584
|
-
dev: receipt.transcriptParentIdentity.dev.toString(),
|
|
1585
|
-
ino: receipt.transcriptParentIdentity.ino.toString(),
|
|
1586
|
-
},
|
|
1587
1635
|
...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
|
|
1588
|
-
...(receipt.artifactsAbsentAtAuthorization ? { artifactsAbsentAtAuthorization: true as const } : {}),
|
|
1589
1636
|
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
1637
|
+
...(receipt.artifactsPayloadDurable === true ? { artifactsPayloadDurable: true } : {}),
|
|
1638
|
+
...(receipt.artifactsRemovedAttempt !== undefined
|
|
1639
|
+
? { artifactsRemovedAttempt: receipt.artifactsRemovedAttempt }
|
|
1640
|
+
: {}),
|
|
1590
1641
|
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
1591
1642
|
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
1643
|
+
...(receipt.transcriptPayloadDurable === true ? { transcriptPayloadDurable: true } : {}),
|
|
1592
1644
|
...(receipt.retainedArtifactsSuccessorPath
|
|
1593
1645
|
? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
|
|
1594
1646
|
: {}),
|
|
@@ -1612,45 +1664,102 @@ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: Cleanup
|
|
|
1612
1664
|
};
|
|
1613
1665
|
}
|
|
1614
1666
|
|
|
1615
|
-
|
|
1667
|
+
type CleanupArtifactsRemovedEvidence = { retainedArtifactsRootPath?: string };
|
|
1668
|
+
|
|
1669
|
+
function cleanupArtifactsRemovedEvidence(
|
|
1616
1670
|
scope: ManagedScope,
|
|
1617
1671
|
tombstone: string,
|
|
1618
1672
|
target: RetiredTarget,
|
|
1619
1673
|
attempt: number,
|
|
1620
|
-
):
|
|
1674
|
+
): CleanupArtifactsRemovedEvidence | undefined {
|
|
1621
1675
|
try {
|
|
1622
1676
|
const value: unknown = JSON.parse(
|
|
1623
1677
|
captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "artifacts_removed", attempt)).bytes.toString(
|
|
1624
1678
|
"utf8",
|
|
1625
1679
|
),
|
|
1626
1680
|
);
|
|
1627
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
|
1681
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1628
1682
|
const record = value as Record<string, unknown>;
|
|
1629
1683
|
const recorded = record.target as Record<string, unknown> | undefined;
|
|
1630
1684
|
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
1631
|
-
|
|
1632
|
-
record.schemaVersion
|
|
1633
|
-
record.state
|
|
1634
|
-
record.scope
|
|
1635
|
-
record.tombstone
|
|
1636
|
-
record.attempt
|
|
1637
|
-
recorded?.path
|
|
1638
|
-
recorded.sessionId
|
|
1639
|
-
recorded.cwd
|
|
1640
|
-
identity?.canonicalPath
|
|
1641
|
-
identity.dev
|
|
1642
|
-
identity.ino
|
|
1643
|
-
identity.size
|
|
1644
|
-
identity.mtimeNs
|
|
1645
|
-
identity.sha256
|
|
1646
|
-
retainedArtifactsRootMatches(record)
|
|
1647
|
-
)
|
|
1685
|
+
if (
|
|
1686
|
+
record.schemaVersion !== 2 ||
|
|
1687
|
+
record.state !== "artifacts_removed" ||
|
|
1688
|
+
record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
|
|
1689
|
+
record.tombstone !== tombstone ||
|
|
1690
|
+
record.attempt !== attempt ||
|
|
1691
|
+
recorded?.path !== target.path ||
|
|
1692
|
+
recorded.sessionId !== target.sessionId ||
|
|
1693
|
+
recorded.cwd !== target.cwd ||
|
|
1694
|
+
identity?.canonicalPath !== target.identity.canonicalPath ||
|
|
1695
|
+
identity.dev !== String(target.identity.dev) ||
|
|
1696
|
+
identity.ino !== String(target.identity.ino) ||
|
|
1697
|
+
identity.size !== target.identity.size ||
|
|
1698
|
+
identity.mtimeNs !== String(target.identity.mtimeNs) ||
|
|
1699
|
+
identity.sha256 !== target.identity.sha256 ||
|
|
1700
|
+
!retainedArtifactsRootMatches(record)
|
|
1701
|
+
)
|
|
1702
|
+
return undefined;
|
|
1703
|
+
const retained = record.retainedArtifactsRoot as Record<string, unknown> | undefined;
|
|
1704
|
+
return { retainedArtifactsRootPath: typeof retained?.path === "string" ? retained.path : undefined };
|
|
1648
1705
|
} catch (error) {
|
|
1649
1706
|
if ((error as Error).message === "durability_failed") throw error;
|
|
1650
|
-
return
|
|
1707
|
+
return undefined;
|
|
1651
1708
|
}
|
|
1652
1709
|
}
|
|
1653
1710
|
|
|
1711
|
+
function cleanupArtifactsRemovedReceipt(
|
|
1712
|
+
tombstone: string,
|
|
1713
|
+
target: RetiredTarget,
|
|
1714
|
+
attempt: number,
|
|
1715
|
+
): RetainedArtifactsRootReceipt | undefined {
|
|
1716
|
+
const receiptPath = cleanupReceiptPath(tombstone, target, "artifacts_removed", attempt);
|
|
1717
|
+
if (!fs.existsSync(receiptPath)) return undefined;
|
|
1718
|
+
const record = JSON.parse(captureManagedFileNoFollow(receiptPath).bytes.toString("utf8")) as Record<string, unknown>;
|
|
1719
|
+
if (!retainedArtifactsRootMatches(record)) return undefined;
|
|
1720
|
+
const retained = record.retainedArtifactsRoot as Record<string, unknown> | undefined;
|
|
1721
|
+
const identity = retained?.identity;
|
|
1722
|
+
const tree = artifactTreeSnapshot(retained?.tree);
|
|
1723
|
+
if (
|
|
1724
|
+
!retained ||
|
|
1725
|
+
typeof retained.path !== "string" ||
|
|
1726
|
+
!identity ||
|
|
1727
|
+
typeof identity !== "object" ||
|
|
1728
|
+
Array.isArray(identity) ||
|
|
1729
|
+
!tree
|
|
1730
|
+
)
|
|
1731
|
+
return undefined;
|
|
1732
|
+
const typed = identity as Record<string, unknown>;
|
|
1733
|
+
if (
|
|
1734
|
+
typeof typed.dev !== "string" ||
|
|
1735
|
+
typeof typed.ino !== "string" ||
|
|
1736
|
+
typeof typed.size !== "number" ||
|
|
1737
|
+
typeof typed.mtimeNs !== "string" ||
|
|
1738
|
+
typeof typed.sha256 !== "string"
|
|
1739
|
+
)
|
|
1740
|
+
return undefined;
|
|
1741
|
+
return {
|
|
1742
|
+
path: retained.path,
|
|
1743
|
+
identity: {
|
|
1744
|
+
dev: BigInt(typed.dev),
|
|
1745
|
+
ino: BigInt(typed.ino),
|
|
1746
|
+
size: typed.size,
|
|
1747
|
+
mtimeNs: BigInt(typed.mtimeNs),
|
|
1748
|
+
sha256: typed.sha256,
|
|
1749
|
+
},
|
|
1750
|
+
tree,
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function cleanupArtifactsRemoved(
|
|
1755
|
+
scope: ManagedScope,
|
|
1756
|
+
tombstone: string,
|
|
1757
|
+
target: RetiredTarget,
|
|
1758
|
+
attempt: number,
|
|
1759
|
+
): boolean {
|
|
1760
|
+
return cleanupArtifactsRemovedEvidence(scope, tombstone, target, attempt) !== undefined;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1654
1763
|
async function publishCleanupArtifactsRemoved(
|
|
1655
1764
|
scope: ManagedScope,
|
|
1656
1765
|
tombstone: string,
|
|
@@ -1700,6 +1809,23 @@ function isAuthorizedArtifactRoot(target: RetiredTarget, plannedRoot: string, pa
|
|
|
1700
1809
|
);
|
|
1701
1810
|
}
|
|
1702
1811
|
|
|
1812
|
+
function assertAuthorizedCleanupPending(
|
|
1813
|
+
target: RetiredTarget,
|
|
1814
|
+
active: CleanupReceipt,
|
|
1815
|
+
deletion: Extract<VerifiedSessionDeleteResult, { kind: "cleanup_pending" }>,
|
|
1816
|
+
): void {
|
|
1817
|
+
if (
|
|
1818
|
+
(deletion.phase === "artifacts" &&
|
|
1819
|
+
!isAuthorizedArtifactRoot(
|
|
1820
|
+
target,
|
|
1821
|
+
active.detachedArtifactsPath ?? active.plannedArtifactsPath,
|
|
1822
|
+
deletion.detachedArtifactsPath,
|
|
1823
|
+
)) ||
|
|
1824
|
+
(deletion.phase === "transcript" && deletion.detachedTranscriptPath !== active.plannedTranscriptPath)
|
|
1825
|
+
)
|
|
1826
|
+
throw new Error("durability_failed");
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1703
1829
|
function transcriptRootMatchesTarget(pathname: string, target: RetiredTarget): boolean {
|
|
1704
1830
|
try {
|
|
1705
1831
|
const observed = captureManagedFileNoFollow(pathname);
|
|
@@ -1717,16 +1843,36 @@ function transcriptRootMatchesTarget(pathname: string, target: RetiredTarget): b
|
|
|
1717
1843
|
}
|
|
1718
1844
|
}
|
|
1719
1845
|
|
|
1720
|
-
function
|
|
1846
|
+
function reconcileScrubbedTranscriptPlaceholder(pathname: string): boolean {
|
|
1721
1847
|
try {
|
|
1722
|
-
const stat = fs.lstatSync(
|
|
1848
|
+
const stat = fs.lstatSync(pathname);
|
|
1849
|
+
return !stat.isSymbolicLink() && stat.isFile() && stat.size === 0 && stat.nlink === 1;
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function managedPathPresentNoFollow(pathname: string): boolean {
|
|
1856
|
+
try {
|
|
1857
|
+
fs.lstatSync(pathname);
|
|
1858
|
+
return true;
|
|
1859
|
+
} catch (error) {
|
|
1860
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
1861
|
+
throw new Error("durability_failed");
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function isScrubbedTranscriptPlaceholder(pathname: string): boolean {
|
|
1866
|
+
try {
|
|
1867
|
+
const stat = fs.lstatSync(pathname);
|
|
1868
|
+
if (stat.isSymbolicLink()) return false;
|
|
1723
1869
|
return (
|
|
1724
|
-
stat.
|
|
1725
|
-
stat.
|
|
1726
|
-
stat.ino === pending.transcriptParentIdentity.ino
|
|
1870
|
+
(stat.isFile() && stat.size === 0 && stat.nlink === 1) ||
|
|
1871
|
+
(stat.isDirectory() && fs.readdirSync(pathname).length === 0)
|
|
1727
1872
|
);
|
|
1728
|
-
} catch {
|
|
1729
|
-
return false;
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
1875
|
+
throw new Error("durability_failed");
|
|
1730
1876
|
}
|
|
1731
1877
|
}
|
|
1732
1878
|
|
|
@@ -1734,32 +1880,16 @@ function cleanupRootsAbsent(
|
|
|
1734
1880
|
tombstone: string,
|
|
1735
1881
|
target: RetiredTarget,
|
|
1736
1882
|
pending: CleanupReceipt,
|
|
1737
|
-
|
|
1883
|
+
allowedRetainedArtifactsRoot?: string,
|
|
1738
1884
|
): boolean {
|
|
1739
|
-
const transcriptParent = path.dirname(target.path);
|
|
1740
|
-
const transcriptParentMatches = (): boolean => {
|
|
1741
|
-
try {
|
|
1742
|
-
const stat = fs.lstatSync(transcriptParent, { bigint: true });
|
|
1743
|
-
return (
|
|
1744
|
-
stat.isDirectory() &&
|
|
1745
|
-
stat.dev === pending.transcriptParentIdentity.dev &&
|
|
1746
|
-
stat.ino === pending.transcriptParentIdentity.ino
|
|
1747
|
-
);
|
|
1748
|
-
} catch {
|
|
1749
|
-
return false;
|
|
1750
|
-
}
|
|
1751
|
-
};
|
|
1752
|
-
if (!transcriptParentMatches()) return false;
|
|
1753
1885
|
const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
|
|
1754
|
-
const activeTranscriptRoots =
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
)
|
|
1760
|
-
: new Set<string>();
|
|
1886
|
+
const activeTranscriptRoots = new Set(
|
|
1887
|
+
[pending.plannedTranscriptPath, pending.detachedTranscriptPath].filter((pathname): pathname is string =>
|
|
1888
|
+
isQuarantinePath(target, pathname),
|
|
1889
|
+
),
|
|
1890
|
+
);
|
|
1761
1891
|
const roots = new Set<string>([
|
|
1762
|
-
|
|
1892
|
+
target.path,
|
|
1763
1893
|
target.path.slice(0, -6),
|
|
1764
1894
|
pending.plannedArtifactsPath,
|
|
1765
1895
|
deterministicRemovalRoot(pending.plannedArtifactsPath),
|
|
@@ -1788,25 +1918,80 @@ function cleanupRootsAbsent(
|
|
|
1788
1918
|
retainedTranscriptSuccessorPath?: unknown;
|
|
1789
1919
|
retainedTranscriptPlaceholderPath?: unknown;
|
|
1790
1920
|
retainedTranscriptUnknownPath?: unknown;
|
|
1921
|
+
transcriptPayloadDurable?: unknown;
|
|
1791
1922
|
};
|
|
1792
1923
|
if (isQuarantinePath(target, record.plannedArtifactsPath)) {
|
|
1793
1924
|
roots.add(record.plannedArtifactsPath);
|
|
1794
1925
|
roots.add(deterministicRemovalRoot(record.plannedArtifactsPath));
|
|
1795
1926
|
}
|
|
1796
1927
|
if (isQuarantinePath(target, record.detachedArtifactsPath)) roots.add(record.detachedArtifactsPath);
|
|
1928
|
+
const historicalTranscriptDurable = record.transcriptPayloadDurable === true;
|
|
1797
1929
|
for (const pathname of [record.plannedTranscriptPath, record.detachedTranscriptPath]) {
|
|
1798
|
-
if (isQuarantinePath(target, pathname)
|
|
1930
|
+
if (!isQuarantinePath(target, pathname) || activeTranscriptRoots.has(pathname)) continue;
|
|
1931
|
+
if (historicalTranscriptDurable || pending.transcriptPayloadDurable === true) {
|
|
1932
|
+
if (!reconcileScrubbedTranscriptPlaceholder(pathname)) return false;
|
|
1933
|
+
roots.delete(pathname);
|
|
1934
|
+
} else roots.add(pathname);
|
|
1799
1935
|
}
|
|
1800
|
-
for (const pathname of [
|
|
1801
|
-
record.retainedArtifactsSuccessorPath,
|
|
1802
|
-
record.retainedArtifactsPlaceholderPath,
|
|
1803
|
-
record.retainedArtifactsUnknownPath,
|
|
1804
|
-
record.retainedTranscriptSuccessorPath,
|
|
1805
|
-
record.retainedTranscriptPlaceholderPath,
|
|
1806
|
-
record.retainedTranscriptUnknownPath,
|
|
1807
|
-
]) {
|
|
1936
|
+
for (const pathname of [record.retainedArtifactsSuccessorPath, record.retainedArtifactsUnknownPath]) {
|
|
1808
1937
|
if (isRetainedNativePath(target, pathname)) roots.add(pathname);
|
|
1809
1938
|
}
|
|
1939
|
+
if (isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) {
|
|
1940
|
+
if (!reconcileScrubbedTranscriptPlaceholder(record.retainedArtifactsPlaceholderPath))
|
|
1941
|
+
roots.add(record.retainedArtifactsPlaceholderPath);
|
|
1942
|
+
}
|
|
1943
|
+
for (const pathname of [record.retainedTranscriptSuccessorPath, record.retainedTranscriptUnknownPath]) {
|
|
1944
|
+
if (isRetainedNativePath(target, pathname)) roots.add(pathname);
|
|
1945
|
+
}
|
|
1946
|
+
if (isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) {
|
|
1947
|
+
if (historicalTranscriptDurable) {
|
|
1948
|
+
if (!reconcileScrubbedTranscriptPlaceholder(record.retainedTranscriptPlaceholderPath)) return false;
|
|
1949
|
+
roots.delete(record.retainedTranscriptPlaceholderPath);
|
|
1950
|
+
} else roots.add(record.retainedTranscriptPlaceholderPath);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
for (const blocker of [pending.retainedArtifactsSuccessorPath, pending.retainedArtifactsUnknownPath]) {
|
|
1954
|
+
if (!blocker) continue;
|
|
1955
|
+
try {
|
|
1956
|
+
fs.lstatSync(blocker);
|
|
1957
|
+
return false;
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (pending.retainedArtifactsPlaceholderPath) {
|
|
1963
|
+
if (!reconcileScrubbedTranscriptPlaceholder(pending.retainedArtifactsPlaceholderPath)) return false;
|
|
1964
|
+
roots.delete(pending.retainedArtifactsPlaceholderPath);
|
|
1965
|
+
}
|
|
1966
|
+
if (pending.transcriptPayloadDurable === true) {
|
|
1967
|
+
for (const blocker of [pending.retainedTranscriptSuccessorPath, pending.retainedTranscriptUnknownPath]) {
|
|
1968
|
+
if (!blocker) continue;
|
|
1969
|
+
try {
|
|
1970
|
+
fs.lstatSync(blocker);
|
|
1971
|
+
return false;
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
for (const placeholder of [
|
|
1977
|
+
target.path,
|
|
1978
|
+
pending.plannedTranscriptPath,
|
|
1979
|
+
pending.detachedTranscriptPath,
|
|
1980
|
+
pending.retainedTranscriptPlaceholderPath,
|
|
1981
|
+
].filter((pathname): pathname is string => typeof pathname === "string")) {
|
|
1982
|
+
if (!reconcileScrubbedTranscriptPlaceholder(placeholder)) return false;
|
|
1983
|
+
roots.delete(placeholder);
|
|
1984
|
+
activeTranscriptRoots.delete(placeholder);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
if (allowedRetainedArtifactsRoot) {
|
|
1988
|
+
if (
|
|
1989
|
+
pending.detachedArtifactsPath !== allowedRetainedArtifactsRoot ||
|
|
1990
|
+
!retainedArtifactPayloadAbsent(allowedRetainedArtifactsRoot)
|
|
1991
|
+
)
|
|
1992
|
+
return false;
|
|
1993
|
+
assertRetainedArtifactsAuthority(pending);
|
|
1994
|
+
roots.delete(allowedRetainedArtifactsRoot);
|
|
1810
1995
|
}
|
|
1811
1996
|
for (const pathname of roots) {
|
|
1812
1997
|
try {
|
|
@@ -1827,75 +2012,85 @@ function cleanupRootsAbsent(
|
|
|
1827
2012
|
if (!transcriptRootMatchesTarget(pathname, target)) throw new Error("durability_failed");
|
|
1828
2013
|
return false;
|
|
1829
2014
|
}
|
|
1830
|
-
if (
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
let entryCount = 0;
|
|
1834
|
-
try {
|
|
1835
|
-
while (pendingDirectories.length > 0) {
|
|
1836
|
-
const directory = pendingDirectories.pop();
|
|
1837
|
-
if (!directory) return false;
|
|
1838
|
-
const before = fs.lstatSync(directory, { bigint: true });
|
|
1839
|
-
if (!before.isDirectory()) return false;
|
|
1840
|
-
if (
|
|
1841
|
-
directory === transcriptParent &&
|
|
1842
|
-
(before.dev !== pending.transcriptParentIdentity.dev || before.ino !== pending.transcriptParentIdentity.ino)
|
|
1843
|
-
)
|
|
1844
|
-
return false;
|
|
1845
|
-
snapshots.push({ path: directory, stat: before });
|
|
1846
|
-
const entries = fs.readdirSync(directory);
|
|
1847
|
-
entryCount += entries.length;
|
|
1848
|
-
if (entryCount > MANAGED_ARTIFACT_MAX_FILES) return false;
|
|
1849
|
-
for (const name of entries) {
|
|
1850
|
-
const pathname = path.join(directory, name);
|
|
1851
|
-
let stat: fs.BigIntStats;
|
|
1852
|
-
try {
|
|
1853
|
-
stat = fs.lstatSync(pathname, { bigint: true });
|
|
1854
|
-
} catch {
|
|
1855
|
-
return false;
|
|
1856
|
-
}
|
|
1857
|
-
if (stat.dev === target.identity.dev && stat.ino === target.identity.ino) return false;
|
|
1858
|
-
if (stat.isDirectory()) pendingDirectories.push(pathname);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
for (const snapshot of snapshots) {
|
|
1862
|
-
const after = fs.lstatSync(snapshot.path, { bigint: true });
|
|
1863
|
-
if (
|
|
1864
|
-
!after.isDirectory() ||
|
|
1865
|
-
after.dev !== snapshot.stat.dev ||
|
|
1866
|
-
after.ino !== snapshot.stat.ino ||
|
|
1867
|
-
after.mtimeNs !== snapshot.stat.mtimeNs ||
|
|
1868
|
-
after.ctimeNs !== snapshot.stat.ctimeNs
|
|
1869
|
-
)
|
|
1870
|
-
return false;
|
|
1871
|
-
}
|
|
1872
|
-
return true;
|
|
1873
|
-
} catch {
|
|
1874
|
-
return false;
|
|
2015
|
+
if (allowedRetainedArtifactsRoot) {
|
|
2016
|
+
assertRetainedArtifactsAuthority(pending);
|
|
2017
|
+
if (!retainedArtifactPayloadAbsent(allowedRetainedArtifactsRoot)) return false;
|
|
1875
2018
|
}
|
|
2019
|
+
return true;
|
|
1876
2020
|
}
|
|
1877
2021
|
|
|
1878
2022
|
function sameArtifactRootIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
|
|
1879
2023
|
return left.dev === right.dev && left.ino === right.ino;
|
|
1880
2024
|
}
|
|
1881
2025
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2026
|
+
type ManagedArtifactTreeEntry = NativeDirectoryTreeSnapshot["entries"][number];
|
|
2027
|
+
|
|
2028
|
+
function artifactTreeQuarantineName(entry: ManagedArtifactTreeEntry): string {
|
|
2029
|
+
const material = Buffer.concat([
|
|
2030
|
+
Buffer.from(entry.relativePath),
|
|
2031
|
+
Buffer.from([0]),
|
|
2032
|
+
Buffer.from(entry.dev),
|
|
2033
|
+
Buffer.from([0]),
|
|
2034
|
+
Buffer.from(entry.ino),
|
|
2035
|
+
]);
|
|
2036
|
+
return `.pi-tree-detached-${createHash("sha256").update(material).digest("hex")}`;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function artifactTreeReplayPathCompatible(
|
|
2040
|
+
observedPath: string,
|
|
2041
|
+
expectedPath: string,
|
|
2042
|
+
expectedByPath: ReadonlyMap<string, ManagedArtifactTreeEntry>,
|
|
2043
|
+
): boolean {
|
|
2044
|
+
if (expectedPath === "") return observedPath === "";
|
|
2045
|
+
const observedParts = observedPath.split("/");
|
|
2046
|
+
const expectedParts = expectedPath.split("/");
|
|
2047
|
+
if (observedParts.length !== expectedParts.length) return false;
|
|
2048
|
+
for (let index = 0; index < expectedParts.length; index += 1) {
|
|
2049
|
+
const logicalPath = expectedParts.slice(0, index + 1).join("/");
|
|
2050
|
+
const logicalEntry = expectedByPath.get(logicalPath);
|
|
2051
|
+
if (!logicalEntry) return false;
|
|
2052
|
+
const observedPart = observedParts[index];
|
|
2053
|
+
if (observedPart !== expectedParts[index] && observedPart !== artifactTreeQuarantineName(logicalEntry))
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
return true;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
export function artifactTreeReplayCompatible(
|
|
2060
|
+
observed: NativeDirectoryTreeSnapshot,
|
|
2061
|
+
expected: NativeDirectoryTreeSnapshot,
|
|
2062
|
+
): boolean {
|
|
2063
|
+
if (observed.rootDev !== expected.rootDev || observed.rootIno !== expected.rootIno) return false;
|
|
2064
|
+
const emptyDigest = createHash("sha256").update("").digest("hex");
|
|
2065
|
+
const expectedByIdentity = new Map(
|
|
2066
|
+
expected.entries.map(entry => [JSON.stringify([entry.kind, entry.dev, entry.ino]), entry] as const),
|
|
2067
|
+
);
|
|
2068
|
+
const expectedByPath = new Map(expected.entries.map(entry => [entry.relativePath, entry] as const));
|
|
2069
|
+
const seen = new Set<string>();
|
|
2070
|
+
let rootObserved = false;
|
|
2071
|
+
const compatible = observed.entries.every(entry => {
|
|
2072
|
+
const key = JSON.stringify([entry.kind, entry.dev, entry.ino]);
|
|
2073
|
+
if (seen.has(key)) return false;
|
|
2074
|
+
seen.add(key);
|
|
2075
|
+
const original = expectedByIdentity.get(key);
|
|
2076
|
+
if (!original || !artifactTreeReplayPathCompatible(entry.relativePath, original.relativePath, expectedByPath))
|
|
2077
|
+
return false;
|
|
2078
|
+
if (entry.relativePath === "" && entry.kind === "directory") rootObserved = true;
|
|
2079
|
+
if (entry.kind === "directory") return true;
|
|
2080
|
+
return (
|
|
2081
|
+
(entry.size === original.size && entry.mtimeNs === original.mtimeNs && entry.sha256 === original.sha256) ||
|
|
2082
|
+
(entry.size === "0" && entry.sha256 === emptyDigest)
|
|
2083
|
+
);
|
|
2084
|
+
});
|
|
2085
|
+
return compatible && rootObserved;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function artifactTreePayloadAbsent(snapshot: NativeDirectoryTreeSnapshot): boolean {
|
|
2089
|
+
const emptyDigest = createHash("sha256").update("").digest("hex");
|
|
2090
|
+
return snapshot.entries.every(
|
|
2091
|
+
entry =>
|
|
2092
|
+
entry.kind === "directory" || (entry.kind === "file" && entry.size === "0" && entry.sha256 === emptyDigest),
|
|
2093
|
+
);
|
|
1899
2094
|
}
|
|
1900
2095
|
|
|
1901
2096
|
function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
@@ -1908,10 +2103,15 @@ function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
|
1908
2103
|
throw new Error("durability_failed");
|
|
1909
2104
|
}
|
|
1910
2105
|
const observed = artifactIdentityAt(pending.detachedArtifactsPath);
|
|
2106
|
+
const observedTree = snapshotArtifactTree(pending.detachedArtifactsPath);
|
|
1911
2107
|
if (
|
|
1912
2108
|
!observed ||
|
|
1913
|
-
|
|
1914
|
-
|
|
2109
|
+
(pending.artifactsPayloadDurable === true
|
|
2110
|
+
? observed.dev !== pending.expectedArtifactsIdentity.dev ||
|
|
2111
|
+
observed.ino !== pending.expectedArtifactsIdentity.ino ||
|
|
2112
|
+
!artifactTreePayloadAbsent(observedTree)
|
|
2113
|
+
: !sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
|
|
2114
|
+
!artifactTreeReplayCompatible(observedTree, pending.expectedArtifactsTree))
|
|
1915
2115
|
)
|
|
1916
2116
|
throw new Error("binding_invalid");
|
|
1917
2117
|
}
|
|
@@ -1921,27 +2121,77 @@ function probePlannedCleanupDetach(target: RetiredTarget, pending: CleanupReceip
|
|
|
1921
2121
|
|
|
1922
2122
|
let detachedArtifactsPath = pending.detachedArtifactsPath;
|
|
1923
2123
|
let detachedTranscriptPath = pending.detachedTranscriptPath;
|
|
1924
|
-
|
|
2124
|
+
const detachedCandidates = new Set<string>([
|
|
2125
|
+
pending.plannedArtifactsPath,
|
|
2126
|
+
deterministicRemovalRoot(pending.plannedArtifactsPath),
|
|
2127
|
+
...(pending.detachedArtifactsPath ? [deterministicRemovalRoot(pending.detachedArtifactsPath)] : []),
|
|
2128
|
+
]);
|
|
2129
|
+
for (const pathname of detachedCandidates) {
|
|
1925
2130
|
if (!fs.existsSync(pathname)) continue;
|
|
1926
|
-
if (!pending.expectedArtifactsIdentity) throw new Error("durability_failed");
|
|
2131
|
+
if (!pending.expectedArtifactsIdentity || !pending.expectedArtifactsTree) throw new Error("durability_failed");
|
|
1927
2132
|
const observed = artifactIdentityAt(pathname);
|
|
1928
|
-
|
|
1929
|
-
throw new Error("durability_failed");
|
|
1930
|
-
if (detachedArtifactsPath && detachedArtifactsPath !== pathname) throw new Error("durability_failed");
|
|
1931
|
-
detachedArtifactsPath = pathname;
|
|
1932
|
-
}
|
|
1933
|
-
if (fs.existsSync(pending.plannedTranscriptPath)) {
|
|
1934
|
-
const observed = captureManagedFileNoFollow(pending.plannedTranscriptPath);
|
|
1935
|
-
const digest = createHash("sha256").update(observed.bytes).digest("hex");
|
|
2133
|
+
const observedTree = snapshotArtifactTree(pathname);
|
|
1936
2134
|
if (
|
|
1937
|
-
observed
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2135
|
+
!observed ||
|
|
2136
|
+
(pending.artifactsPayloadDurable === true
|
|
2137
|
+
? observed.dev !== pending.expectedArtifactsIdentity.dev ||
|
|
2138
|
+
observed.ino !== pending.expectedArtifactsIdentity.ino ||
|
|
2139
|
+
!artifactTreePayloadAbsent(observedTree)
|
|
2140
|
+
: !sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
|
|
2141
|
+
!artifactTreeReplayCompatible(observedTree, pending.expectedArtifactsTree))
|
|
1942
2142
|
)
|
|
1943
2143
|
throw new Error("durability_failed");
|
|
1944
|
-
|
|
2144
|
+
if (detachedArtifactsPath && detachedArtifactsPath !== pathname && fs.existsSync(detachedArtifactsPath))
|
|
2145
|
+
throw new Error("durability_failed");
|
|
2146
|
+
detachedArtifactsPath = pathname;
|
|
2147
|
+
}
|
|
2148
|
+
if (pending.transcriptPayloadDurable === true) {
|
|
2149
|
+
for (const blocker of [pending.retainedTranscriptSuccessorPath, pending.retainedTranscriptUnknownPath]) {
|
|
2150
|
+
if (!blocker) continue;
|
|
2151
|
+
try {
|
|
2152
|
+
fs.lstatSync(blocker);
|
|
2153
|
+
throw new Error("durability_failed");
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
if ((error as Error).message === "durability_failed") throw error;
|
|
2156
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw new Error("durability_failed");
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
for (const placeholder of [
|
|
2160
|
+
target.path,
|
|
2161
|
+
pending.plannedTranscriptPath,
|
|
2162
|
+
pending.detachedTranscriptPath,
|
|
2163
|
+
pending.retainedTranscriptPlaceholderPath,
|
|
2164
|
+
].filter((pathname): pathname is string => typeof pathname === "string"))
|
|
2165
|
+
if (!reconcileScrubbedTranscriptPlaceholder(placeholder)) throw new Error("durability_failed");
|
|
2166
|
+
return { ...pending, detachedArtifactsPath, detachedTranscriptPath: undefined };
|
|
2167
|
+
}
|
|
2168
|
+
const retainedBlockers = [pending.retainedTranscriptSuccessorPath, pending.retainedTranscriptUnknownPath].filter(
|
|
2169
|
+
(pathname): pathname is string => typeof pathname === "string" && managedPathPresentNoFollow(pathname),
|
|
2170
|
+
);
|
|
2171
|
+
if (retainedBlockers.length > 0) throw new Error("durability_failed");
|
|
2172
|
+
const transcriptCandidates = [
|
|
2173
|
+
target.path,
|
|
2174
|
+
pending.plannedTranscriptPath,
|
|
2175
|
+
pending.detachedTranscriptPath,
|
|
2176
|
+
pending.retainedTranscriptPlaceholderPath,
|
|
2177
|
+
].filter(
|
|
2178
|
+
(pathname, index, values): pathname is string =>
|
|
2179
|
+
typeof pathname === "string" && managedPathPresentNoFollow(pathname) && values.indexOf(pathname) === index,
|
|
2180
|
+
);
|
|
2181
|
+
const boundCandidates = transcriptCandidates.filter(pathname => transcriptRootMatchesTarget(pathname, target));
|
|
2182
|
+
if (boundCandidates.length > 1) throw new Error("durability_failed");
|
|
2183
|
+
if (boundCandidates.length === 1) {
|
|
2184
|
+
detachedTranscriptPath = boundCandidates[0] === target.path ? undefined : boundCandidates[0];
|
|
2185
|
+
} else if (transcriptCandidates.length > 0) {
|
|
2186
|
+
if (!transcriptCandidates.every(isScrubbedTranscriptPlaceholder)) throw new Error("durability_failed");
|
|
2187
|
+
for (const pathname of transcriptCandidates)
|
|
2188
|
+
if (!reconcileScrubbedTranscriptPlaceholder(pathname)) throw new Error("durability_failed");
|
|
2189
|
+
return {
|
|
2190
|
+
...pending,
|
|
2191
|
+
detachedArtifactsPath,
|
|
2192
|
+
detachedTranscriptPath: undefined,
|
|
2193
|
+
transcriptPayloadDurable: true,
|
|
2194
|
+
};
|
|
1945
2195
|
}
|
|
1946
2196
|
return { ...pending, detachedArtifactsPath, detachedTranscriptPath };
|
|
1947
2197
|
}
|
|
@@ -1974,7 +2224,6 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
1974
2224
|
(item.kind === "file" || item.kind === "directory") &&
|
|
1975
2225
|
typeof item.dev === "string" &&
|
|
1976
2226
|
typeof item.ino === "string" &&
|
|
1977
|
-
typeof item.nlink === "string" &&
|
|
1978
2227
|
typeof item.size === "string" &&
|
|
1979
2228
|
typeof item.mtimeNs === "string" &&
|
|
1980
2229
|
typeof item.ctimeNs === "string" &&
|
|
@@ -2022,14 +2271,6 @@ function pendingCleanupReceipt(
|
|
|
2022
2271
|
const attempt = record.attempt;
|
|
2023
2272
|
const recorded = record.target as Record<string, unknown> | undefined;
|
|
2024
2273
|
const identity = recorded?.identity as Record<string, unknown> | undefined;
|
|
2025
|
-
const transcriptParentIdentity = record.transcriptParentIdentity as Record<string, unknown> | undefined;
|
|
2026
|
-
if (
|
|
2027
|
-
typeof transcriptParentIdentity?.dev === "string" &&
|
|
2028
|
-
typeof transcriptParentIdentity?.ino === "string" &&
|
|
2029
|
-
(transcriptParentIdentity.dev !== target.transcriptParentIdentity.dev.toString() ||
|
|
2030
|
-
transcriptParentIdentity.ino !== target.transcriptParentIdentity.ino.toString())
|
|
2031
|
-
)
|
|
2032
|
-
throw new Error("durability_failed");
|
|
2033
2274
|
if (
|
|
2034
2275
|
record.schemaVersion !== 2 ||
|
|
2035
2276
|
record.state !== "cleanup_pending" ||
|
|
@@ -2043,20 +2284,9 @@ function pendingCleanupReceipt(
|
|
|
2043
2284
|
recorded.cwd !== target.cwd ||
|
|
2044
2285
|
identity?.dev !== String(target.identity.dev) ||
|
|
2045
2286
|
identity.ino !== String(target.identity.ino) ||
|
|
2046
|
-
identity.nlink !== String(target.identity.nlink) ||
|
|
2047
2287
|
identity.size !== target.identity.size ||
|
|
2048
2288
|
identity.mtimeNs !== String(target.identity.mtimeNs) ||
|
|
2049
2289
|
identity.sha256 !== target.identity.sha256 ||
|
|
2050
|
-
!transcriptParentIdentity ||
|
|
2051
|
-
typeof transcriptParentIdentity.dev !== "string" ||
|
|
2052
|
-
typeof transcriptParentIdentity.ino !== "string" ||
|
|
2053
|
-
!/^[0-9]+$/.test(transcriptParentIdentity.dev) ||
|
|
2054
|
-
!/^[0-9]+$/.test(transcriptParentIdentity.ino) ||
|
|
2055
|
-
transcriptParentIdentity.dev !== target.transcriptParentIdentity.dev.toString() ||
|
|
2056
|
-
transcriptParentIdentity.ino !== target.transcriptParentIdentity.ino.toString() ||
|
|
2057
|
-
(latest !== undefined &&
|
|
2058
|
-
(transcriptParentIdentity.dev !== latest.transcriptParentIdentity.dev.toString() ||
|
|
2059
|
-
transcriptParentIdentity.ino !== latest.transcriptParentIdentity.ino.toString())) ||
|
|
2060
2290
|
!isQuarantinePath(target, record.plannedArtifactsPath) ||
|
|
2061
2291
|
!isQuarantinePath(target, record.plannedTranscriptPath) ||
|
|
2062
2292
|
record.plannedArtifactsPath === record.plannedTranscriptPath ||
|
|
@@ -2075,11 +2305,16 @@ function pendingCleanupReceipt(
|
|
|
2075
2305
|
(record.retainedTranscriptPlaceholderPath !== undefined &&
|
|
2076
2306
|
!isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
|
|
2077
2307
|
(record.retainedTranscriptUnknownPath !== undefined &&
|
|
2078
|
-
!isRetainedNativePath(target, record.retainedTranscriptUnknownPath))
|
|
2308
|
+
!isRetainedNativePath(target, record.retainedTranscriptUnknownPath)) ||
|
|
2309
|
+
(record.artifactsPayloadDurable !== undefined && record.artifactsPayloadDurable !== true) ||
|
|
2310
|
+
(record.artifactsRemovedAttempt !== undefined &&
|
|
2311
|
+
(typeof record.artifactsRemovedAttempt !== "number" ||
|
|
2312
|
+
!Number.isSafeInteger(record.artifactsRemovedAttempt) ||
|
|
2313
|
+
record.artifactsRemovedAttempt < 1 ||
|
|
2314
|
+
record.artifactsRemovedAttempt > (attempt as number))) ||
|
|
2315
|
+
(record.transcriptPayloadDurable !== undefined && record.transcriptPayloadDurable !== true)
|
|
2079
2316
|
)
|
|
2080
2317
|
throw new Error("durability_failed");
|
|
2081
|
-
if (record.artifactsAbsentAtAuthorization !== undefined && record.artifactsAbsentAtAuthorization !== true)
|
|
2082
|
-
throw new Error("durability_failed");
|
|
2083
2318
|
const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
|
|
2084
2319
|
const expectedArtifactsIdentity = artifact
|
|
2085
2320
|
? typeof artifact.dev === "string" &&
|
|
@@ -2097,8 +2332,6 @@ function pendingCleanupReceipt(
|
|
|
2097
2332
|
: undefined
|
|
2098
2333
|
: undefined;
|
|
2099
2334
|
if (artifact && !expectedArtifactsIdentity) throw new Error("durability_failed");
|
|
2100
|
-
if (record.artifactsAbsentAtAuthorization === true && expectedArtifactsIdentity)
|
|
2101
|
-
throw new Error("durability_failed");
|
|
2102
2335
|
const expectedArtifactsTree =
|
|
2103
2336
|
record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
|
|
2104
2337
|
if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
|
|
@@ -2117,15 +2350,13 @@ function pendingCleanupReceipt(
|
|
|
2117
2350
|
latest = {
|
|
2118
2351
|
attempt,
|
|
2119
2352
|
target,
|
|
2120
|
-
transcriptParentIdentity: {
|
|
2121
|
-
dev: BigInt(transcriptParentIdentity.dev as string),
|
|
2122
|
-
ino: BigInt(transcriptParentIdentity.ino as string),
|
|
2123
|
-
},
|
|
2124
2353
|
expectedArtifactsIdentity,
|
|
2125
|
-
artifactsAbsentAtAuthorization: record.artifactsAbsentAtAuthorization === true ? true : undefined,
|
|
2126
2354
|
expectedArtifactsTree,
|
|
2355
|
+
artifactsPayloadDurable: record.artifactsPayloadDurable === true ? true : undefined,
|
|
2356
|
+
artifactsRemovedAttempt: record.artifactsRemovedAttempt as number | undefined,
|
|
2127
2357
|
detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
|
|
2128
2358
|
detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
|
|
2359
|
+
transcriptPayloadDurable: record.transcriptPayloadDurable === true ? true : undefined,
|
|
2129
2360
|
retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
|
|
2130
2361
|
retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
|
|
2131
2362
|
retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
|
|
@@ -2147,13 +2378,7 @@ function artifactIdentityForCleanup(target: RetiredTarget): SessionStorageFileId
|
|
|
2147
2378
|
try {
|
|
2148
2379
|
const stat = fs.lstatSync(target.path.slice(0, -6), { bigint: true });
|
|
2149
2380
|
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("unsafe_artifacts");
|
|
2150
|
-
return {
|
|
2151
|
-
dev: stat.dev,
|
|
2152
|
-
ino: stat.ino,
|
|
2153
|
-
size: Number(stat.size),
|
|
2154
|
-
mtimeNs: stat.mtimeNs,
|
|
2155
|
-
sha256: "",
|
|
2156
|
-
};
|
|
2381
|
+
return { dev: stat.dev, ino: stat.ino, size: Number(stat.size), mtimeNs: stat.mtimeNs, sha256: "" };
|
|
2157
2382
|
} catch (error) {
|
|
2158
2383
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
2159
2384
|
throw error;
|
|
@@ -2172,10 +2397,18 @@ function snapshotArtifactTree(pathname: string): NativeDirectoryTreeSnapshot {
|
|
|
2172
2397
|
return result.snapshot;
|
|
2173
2398
|
}
|
|
2174
2399
|
|
|
2400
|
+
function retainedArtifactPayloadAbsent(pathname: string): boolean {
|
|
2401
|
+
try {
|
|
2402
|
+
return artifactTreePayloadAbsent(snapshotArtifactTree(pathname));
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
2405
|
+
throw error;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2175
2409
|
function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | undefined): CleanupReceipt {
|
|
2176
2410
|
const attempt = (pending?.attempt ?? 0) + 1;
|
|
2177
2411
|
const directory = path.dirname(target.path);
|
|
2178
|
-
const transcriptParentIdentity = pending?.transcriptParentIdentity ?? target.transcriptParentIdentity;
|
|
2179
2412
|
const operation = stableOperationName(target);
|
|
2180
2413
|
const expectedArtifactsIdentity = pending?.expectedArtifactsIdentity ?? artifactIdentityForCleanup(target);
|
|
2181
2414
|
const expectedArtifactsTree =
|
|
@@ -2185,16 +2418,16 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
|
|
|
2185
2418
|
return {
|
|
2186
2419
|
attempt,
|
|
2187
2420
|
target,
|
|
2188
|
-
transcriptParentIdentity,
|
|
2189
2421
|
expectedArtifactsIdentity,
|
|
2190
|
-
artifactsAbsentAtAuthorization:
|
|
2191
|
-
pending?.artifactsAbsentAtAuthorization ?? (expectedArtifactsIdentity ? undefined : true),
|
|
2192
2422
|
expectedArtifactsTree,
|
|
2423
|
+
artifactsPayloadDurable: pending?.artifactsPayloadDurable,
|
|
2424
|
+
artifactsRemovedAttempt: pending?.artifactsRemovedAttempt,
|
|
2193
2425
|
detachedArtifactsPath: pending?.detachedArtifactsPath,
|
|
2194
2426
|
retainedArtifactsSuccessorPath: pending?.retainedArtifactsSuccessorPath,
|
|
2195
2427
|
retainedArtifactsPlaceholderPath: pending?.retainedArtifactsPlaceholderPath,
|
|
2196
2428
|
retainedArtifactsUnknownPath: pending?.retainedArtifactsUnknownPath,
|
|
2197
2429
|
detachedTranscriptPath: pending?.detachedTranscriptPath,
|
|
2430
|
+
transcriptPayloadDurable: pending?.transcriptPayloadDurable,
|
|
2198
2431
|
retainedTranscriptSuccessorPath: pending?.retainedTranscriptSuccessorPath,
|
|
2199
2432
|
retainedTranscriptPlaceholderPath: pending?.retainedTranscriptPlaceholderPath,
|
|
2200
2433
|
retainedTranscriptUnknownPath: pending?.retainedTranscriptUnknownPath,
|
|
@@ -2221,10 +2454,22 @@ function cleanupPendingEvidence(
|
|
|
2221
2454
|
expectedArtifactsIdentity:
|
|
2222
2455
|
deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
|
|
2223
2456
|
expectedArtifactsTree: deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
2457
|
+
artifactsPayloadDurable:
|
|
2458
|
+
deletion.phase === "artifacts"
|
|
2459
|
+
? deletion.artifactsPayloadDurable
|
|
2460
|
+
? true
|
|
2461
|
+
: undefined
|
|
2462
|
+
: active.artifactsPayloadDurable,
|
|
2224
2463
|
detachedArtifactsPath:
|
|
2225
2464
|
deletion.phase === "artifacts" ? deletion.detachedArtifactsPath : active.detachedArtifactsPath,
|
|
2226
2465
|
detachedTranscriptPath:
|
|
2227
2466
|
deletion.phase === "transcript" ? deletion.detachedTranscriptPath : active.detachedTranscriptPath,
|
|
2467
|
+
transcriptPayloadDurable:
|
|
2468
|
+
deletion.phase === "transcript"
|
|
2469
|
+
? deletion.transcriptPayloadDurable
|
|
2470
|
+
? true
|
|
2471
|
+
: undefined
|
|
2472
|
+
: active.transcriptPayloadDurable,
|
|
2228
2473
|
retainedArtifactsSuccessorPath:
|
|
2229
2474
|
deletion.phase === "artifacts" ? deletion.retainedSuccessorPath : active.retainedArtifactsSuccessorPath,
|
|
2230
2475
|
retainedArtifactsPlaceholderPath:
|
|
@@ -2240,6 +2485,57 @@ function cleanupPendingEvidence(
|
|
|
2240
2485
|
};
|
|
2241
2486
|
}
|
|
2242
2487
|
|
|
2488
|
+
async function continueDetachedArtifactCleanup(
|
|
2489
|
+
scope: ManagedScope,
|
|
2490
|
+
tombstone: string,
|
|
2491
|
+
target: RetiredTarget,
|
|
2492
|
+
pendingEvidence: CleanupReceipt,
|
|
2493
|
+
fallbackDetachedTranscriptPath: string | undefined,
|
|
2494
|
+
lock: ManagedStorageLock,
|
|
2495
|
+
flow: ManagedVerifiedDeleteTestEvent["flow"],
|
|
2496
|
+
): Promise<{ deletion: VerifiedSessionDeleteResult; pendingEvidence: CleanupReceipt }> {
|
|
2497
|
+
const deletion = await deleteSessionVerifiedWithFence(flow, "artifact-finalization", lock, {
|
|
2498
|
+
sessionsRoot: scope.sessionsRoot,
|
|
2499
|
+
transcriptPath: target.path,
|
|
2500
|
+
sessionId: target.sessionId,
|
|
2501
|
+
cwd: target.cwd,
|
|
2502
|
+
transcriptIdentity: target.identity,
|
|
2503
|
+
transcriptParentIdentity: (() => {
|
|
2504
|
+
const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
2505
|
+
return { dev: parent.dev, ino: parent.ino };
|
|
2506
|
+
})(),
|
|
2507
|
+
expectedArtifactsIdentity: pendingEvidence.expectedArtifactsIdentity,
|
|
2508
|
+
expectedArtifactsTree: pendingEvidence.expectedArtifactsTree,
|
|
2509
|
+
detachedArtifactsPath: pendingEvidence.detachedArtifactsPath,
|
|
2510
|
+
retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
|
|
2511
|
+
retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
|
|
2512
|
+
retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
|
|
2513
|
+
detachedTranscriptPath: pendingEvidence.detachedTranscriptPath ?? fallbackDetachedTranscriptPath,
|
|
2514
|
+
retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
|
|
2515
|
+
retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
|
|
2516
|
+
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
2517
|
+
plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
|
|
2518
|
+
plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
|
|
2519
|
+
});
|
|
2520
|
+
if (deletion.kind === "cleanup_pending") {
|
|
2521
|
+
if (
|
|
2522
|
+
deletion.phase !== "artifacts" ||
|
|
2523
|
+
!isAuthorizedArtifactRoot(
|
|
2524
|
+
target,
|
|
2525
|
+
pendingEvidence.detachedArtifactsPath ?? pendingEvidence.plannedArtifactsPath,
|
|
2526
|
+
deletion.detachedArtifactsPath,
|
|
2527
|
+
)
|
|
2528
|
+
)
|
|
2529
|
+
throw new Error("durability_failed");
|
|
2530
|
+
const followup = nextCleanupReceipt(target, pendingEvidence);
|
|
2531
|
+
pendingEvidence = cleanupPendingEvidence(followup, pendingEvidence, deletion);
|
|
2532
|
+
await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
|
|
2533
|
+
} else if (deletion.kind !== "artifacts_removed") {
|
|
2534
|
+
throw new Error("durability_failed");
|
|
2535
|
+
}
|
|
2536
|
+
return { deletion, pendingEvidence };
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2243
2539
|
async function publishCleanupPending(
|
|
2244
2540
|
scope: ManagedScope,
|
|
2245
2541
|
tombstone: string,
|
|
@@ -2477,7 +2773,7 @@ function manifestContains(transcriptPath: string, manifest: readonly ArtifactMan
|
|
|
2477
2773
|
type DetachedArtifactRoot = {
|
|
2478
2774
|
originalPath: string;
|
|
2479
2775
|
detachedPath: string;
|
|
2480
|
-
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint };
|
|
2776
|
+
identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; parentDev: bigint; parentIno: bigint };
|
|
2481
2777
|
tree: NativeDirectoryTreeSnapshot;
|
|
2482
2778
|
};
|
|
2483
2779
|
|
|
@@ -2494,6 +2790,7 @@ function planArtifactRootForMigration(sourceTranscript: string, operation: strin
|
|
|
2494
2790
|
const tree = snapshotArtifactTree(originalPath);
|
|
2495
2791
|
const root = tree.entries.find(entry => entry.relativePath === "" && entry.kind === "directory");
|
|
2496
2792
|
if (!root) throw new Error("unsafe_artifacts");
|
|
2793
|
+
const parent = fs.lstatSync(path.dirname(originalPath), { bigint: true });
|
|
2497
2794
|
return {
|
|
2498
2795
|
originalPath,
|
|
2499
2796
|
detachedPath: path.join(path.dirname(originalPath), `.gjc-migrate-${operation}-artifacts`),
|
|
@@ -2502,6 +2799,8 @@ function planArtifactRootForMigration(sourceTranscript: string, operation: strin
|
|
|
2502
2799
|
ino: stat.ino,
|
|
2503
2800
|
size: process.platform === "win32" ? BigInt(root.size) : stat.size,
|
|
2504
2801
|
mtimeNs: process.platform === "win32" ? BigInt(root.mtimeNs) : stat.mtimeNs,
|
|
2802
|
+
parentDev: parent.dev,
|
|
2803
|
+
parentIno: parent.ino,
|
|
2505
2804
|
},
|
|
2506
2805
|
tree,
|
|
2507
2806
|
};
|
|
@@ -2581,7 +2880,6 @@ function sameArtifactTree(
|
|
|
2581
2880
|
entry.kind,
|
|
2582
2881
|
entry.dev,
|
|
2583
2882
|
entry.ino,
|
|
2584
|
-
entry.nlink,
|
|
2585
2883
|
entry.size,
|
|
2586
2884
|
entry.mtimeNs,
|
|
2587
2885
|
entry.ctimeNs,
|
|
@@ -2620,12 +2918,15 @@ export function cleanupAuthorityMatches(
|
|
|
2620
2918
|
): boolean {
|
|
2621
2919
|
try {
|
|
2622
2920
|
const stat = fs.lstatSync(cleanup.retainedPath, { bigint: true });
|
|
2921
|
+
const parentStat = fs.lstatSync(parent, { bigint: true });
|
|
2623
2922
|
if (
|
|
2624
2923
|
path.dirname(cleanup.retainedPath) !== parent ||
|
|
2625
2924
|
!stat.isDirectory() ||
|
|
2626
2925
|
stat.isSymbolicLink() ||
|
|
2627
2926
|
stat.dev !== cleanup.identity.dev ||
|
|
2628
|
-
stat.ino !== cleanup.identity.ino
|
|
2927
|
+
stat.ino !== cleanup.identity.ino ||
|
|
2928
|
+
parentStat.dev !== cleanup.identity.parentDev ||
|
|
2929
|
+
parentStat.ino !== cleanup.identity.parentIno
|
|
2629
2930
|
)
|
|
2630
2931
|
return false;
|
|
2631
2932
|
const snapshot = native.snapshotDirectoryTree(cleanup.retainedPath);
|
|
@@ -2701,6 +3002,7 @@ export function detachArtifactRootForMigration(
|
|
|
2701
3002
|
entry => entry.relativePath === "" && entry.kind === "directory",
|
|
2702
3003
|
);
|
|
2703
3004
|
if (!placeholderRoot) throw new Error("durability_failed");
|
|
3005
|
+
const parent = fs.lstatSync(path.dirname(placeholder), { bigint: true });
|
|
2704
3006
|
const cleanup: SourceArtifactCleanup = {
|
|
2705
3007
|
state: "cleanup_pending",
|
|
2706
3008
|
role: "exchange_placeholder",
|
|
@@ -2710,6 +3012,8 @@ export function detachArtifactRootForMigration(
|
|
|
2710
3012
|
ino: stat.ino,
|
|
2711
3013
|
size: platform === "win32" ? BigInt(placeholderRoot.size) : stat.size,
|
|
2712
3014
|
mtimeNs: platform === "win32" ? BigInt(placeholderRoot.mtimeNs) : stat.mtimeNs,
|
|
3015
|
+
parentDev: parent.dev,
|
|
3016
|
+
parentIno: parent.ino,
|
|
2713
3017
|
},
|
|
2714
3018
|
tree: snapshot.snapshot,
|
|
2715
3019
|
};
|
|
@@ -2762,7 +3066,9 @@ export function restorePreparedArtifactRoot(
|
|
|
2762
3066
|
typeof identity.dev !== "string" ||
|
|
2763
3067
|
typeof identity.ino !== "string" ||
|
|
2764
3068
|
typeof identity.size !== "string" ||
|
|
2765
|
-
typeof identity.mtimeNs !== "string"
|
|
3069
|
+
typeof identity.mtimeNs !== "string" ||
|
|
3070
|
+
typeof identity.parentDev !== "string" ||
|
|
3071
|
+
typeof identity.parentIno !== "string"
|
|
2766
3072
|
)
|
|
2767
3073
|
throw new Error("durability_failed");
|
|
2768
3074
|
const artifactIdentity = {
|
|
@@ -2770,6 +3076,8 @@ export function restorePreparedArtifactRoot(
|
|
|
2770
3076
|
ino: BigInt(identity.ino),
|
|
2771
3077
|
size: BigInt(identity.size),
|
|
2772
3078
|
mtimeNs: BigInt(identity.mtimeNs),
|
|
3079
|
+
parentDev: BigInt(identity.parentDev),
|
|
3080
|
+
parentIno: BigInt(identity.parentIno),
|
|
2773
3081
|
};
|
|
2774
3082
|
const expectedTree = artifactTreeSnapshot(quarantine.tree)!;
|
|
2775
3083
|
const assertPreparedTree = (pathname: string): void => {
|
|
@@ -2814,6 +3122,8 @@ export function restorePreparedArtifactRoot(
|
|
|
2814
3122
|
typeof cleanupIdentity.ino !== "string" ||
|
|
2815
3123
|
typeof cleanupIdentity.size !== "string" ||
|
|
2816
3124
|
typeof cleanupIdentity.mtimeNs !== "string" ||
|
|
3125
|
+
typeof cleanupIdentity.parentDev !== "string" ||
|
|
3126
|
+
typeof cleanupIdentity.parentIno !== "string" ||
|
|
2817
3127
|
!cleanupTree ||
|
|
2818
3128
|
!cleanupAuthorityMatches(
|
|
2819
3129
|
{
|
|
@@ -2825,6 +3135,8 @@ export function restorePreparedArtifactRoot(
|
|
|
2825
3135
|
ino: BigInt(cleanupIdentity.ino),
|
|
2826
3136
|
size: BigInt(cleanupIdentity.size),
|
|
2827
3137
|
mtimeNs: BigInt(cleanupIdentity.mtimeNs),
|
|
3138
|
+
parentDev: BigInt(cleanupIdentity.parentDev),
|
|
3139
|
+
parentIno: BigInt(cleanupIdentity.parentIno),
|
|
2828
3140
|
},
|
|
2829
3141
|
tree: cleanupTree,
|
|
2830
3142
|
},
|
|
@@ -2858,7 +3170,7 @@ export function restorePreparedArtifactRoot(
|
|
|
2858
3170
|
...artifactIdentity,
|
|
2859
3171
|
directory: true,
|
|
2860
3172
|
});
|
|
2861
|
-
if (!result.ok) throw new Error("durability_failed");
|
|
3173
|
+
if (!result.ok && result.code !== "cleanup_pending") throw new Error("durability_failed");
|
|
2862
3174
|
}
|
|
2863
3175
|
|
|
2864
3176
|
function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot, cleanup?: SourceArtifactCleanup): void {
|
|
@@ -2868,7 +3180,7 @@ function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot, cleanup?: S
|
|
|
2868
3180
|
...detached.identity,
|
|
2869
3181
|
directory: true,
|
|
2870
3182
|
});
|
|
2871
|
-
if (!result.ok) throw new Error("durability_failed");
|
|
3183
|
+
if (!result.ok && result.code !== "cleanup_pending") throw new Error("durability_failed");
|
|
2872
3184
|
}
|
|
2873
3185
|
|
|
2874
3186
|
async function copyArtifacts(
|
|
@@ -2972,14 +3284,19 @@ function detachedReceiptMatches(receipt: string, expected: Uint8Array): boolean
|
|
|
2972
3284
|
}
|
|
2973
3285
|
|
|
2974
3286
|
async function removeStagedReceipts(scope: ManagedScope, candidate: ManagedCandidate): Promise<void> {
|
|
3287
|
+
const authority = boundManagedWriteAuthorities.get(scope);
|
|
3288
|
+
const rootAuthority = authority?.rootAuthority ?? scopeRoot(scope);
|
|
3289
|
+
const store = authority?.retainedAuthority
|
|
3290
|
+
? new ManagedSessionDescendantStore(rootAuthority, scope.directoryPath, {
|
|
3291
|
+
authority: authority.retainedAuthority,
|
|
3292
|
+
authorityBaseDir: scope.directoryPath,
|
|
3293
|
+
})
|
|
3294
|
+
: new ManagedSessionDescendantStore(rootAuthority, scope.directoryPath);
|
|
2975
3295
|
for (const state of ["prepared", "detached", "published"] as const) {
|
|
2976
3296
|
const pathname = receiptPathFor(scope, candidate, state);
|
|
2977
3297
|
try {
|
|
2978
|
-
const
|
|
2979
|
-
|
|
2980
|
-
await fs.promises.unlink(pathname);
|
|
2981
|
-
fsyncManagedParent(pathname);
|
|
2982
|
-
}
|
|
3298
|
+
const snapshot = captureManagedFileNoFollow(pathname);
|
|
3299
|
+
store.removeExpected(path.relative(scope.directoryPath, pathname), snapshot);
|
|
2983
3300
|
} catch (error) {
|
|
2984
3301
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
2985
3302
|
}
|
|
@@ -3060,12 +3377,31 @@ export async function reconcileManagedTombstones(
|
|
|
3060
3377
|
fs.lstatSync(target.path);
|
|
3061
3378
|
} catch (error) {
|
|
3062
3379
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
3063
|
-
|
|
3380
|
+
const replayReceipt = observedPending ?? nextCleanupReceipt(target, pending);
|
|
3381
|
+
const artifactsEvidence = cleanupArtifactsRemovedEvidence(
|
|
3382
|
+
scope,
|
|
3383
|
+
tombstone,
|
|
3384
|
+
target,
|
|
3385
|
+
pending?.artifactsRemovedAttempt ??
|
|
3386
|
+
replayReceipt.artifactsRemovedAttempt ??
|
|
3387
|
+
pending?.attempt ??
|
|
3388
|
+
replayReceipt.attempt,
|
|
3389
|
+
);
|
|
3064
3390
|
if (
|
|
3065
|
-
|
|
3066
|
-
cleanupRootsAbsent(
|
|
3067
|
-
|
|
3391
|
+
artifactsEvidence &&
|
|
3392
|
+
cleanupRootsAbsent(
|
|
3393
|
+
tombstone,
|
|
3394
|
+
target,
|
|
3395
|
+
replayReceipt,
|
|
3396
|
+
artifactsEvidence.retainedArtifactsRootPath,
|
|
3397
|
+
)
|
|
3398
|
+
) {
|
|
3399
|
+
fsyncManagedParent(target.path);
|
|
3400
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
3068
3401
|
continue;
|
|
3402
|
+
}
|
|
3403
|
+
if (!observedPending) continue;
|
|
3404
|
+
if (!observedPending.detachedTranscriptPath) continue;
|
|
3069
3405
|
} else throw error;
|
|
3070
3406
|
}
|
|
3071
3407
|
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
@@ -3074,22 +3410,27 @@ export async function reconcileManagedTombstones(
|
|
|
3074
3410
|
!!observedPending &&
|
|
3075
3411
|
(observedPending.detachedArtifactsPath !== pending?.detachedArtifactsPath ||
|
|
3076
3412
|
observedPending.detachedTranscriptPath !== pending?.detachedTranscriptPath);
|
|
3077
|
-
|
|
3413
|
+
let active =
|
|
3078
3414
|
discoveredDetach || (observedPending && requiresFreshCleanupPlan(observedPending))
|
|
3079
3415
|
? nextCleanupReceipt(target, observedPending)
|
|
3080
3416
|
: (observedPending ?? nextCleanupReceipt(target, undefined));
|
|
3081
3417
|
if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
|
|
3082
3418
|
await publishCleanupPending(scope, tombstone, active, lock);
|
|
3083
|
-
|
|
3084
|
-
|
|
3419
|
+
const initialTarget = observedPending?.detachedTranscriptPath
|
|
3420
|
+
? target
|
|
3421
|
+
: validateCandidateForScope(scope, target);
|
|
3422
|
+
if (!initialTarget) throw new Error("source_changed");
|
|
3423
|
+
let deletion = await deleteSessionVerifiedWithFence("reconcile", "initial", lock, {
|
|
3085
3424
|
sessionsRoot: scope.sessionsRoot,
|
|
3086
3425
|
transcriptPath: target.path,
|
|
3087
3426
|
sessionId: target.sessionId,
|
|
3088
3427
|
cwd: target.cwd,
|
|
3089
|
-
transcriptIdentity:
|
|
3090
|
-
|
|
3428
|
+
transcriptIdentity: {
|
|
3429
|
+
...initialTarget.identity,
|
|
3430
|
+
nlink: fs.lstatSync(observedPending?.detachedTranscriptPath ?? target.path, { bigint: true })
|
|
3431
|
+
.nlink,
|
|
3432
|
+
},
|
|
3091
3433
|
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
3092
|
-
artifactsAbsentAtAuthorization: active.artifactsAbsentAtAuthorization,
|
|
3093
3434
|
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
3094
3435
|
detachedArtifactsPath:
|
|
3095
3436
|
active.detachedArtifactsPath ??
|
|
@@ -3110,85 +3451,156 @@ export async function reconcileManagedTombstones(
|
|
|
3110
3451
|
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
3111
3452
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3112
3453
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3113
|
-
...(cleanupArtifactsRemoved(
|
|
3454
|
+
...(cleanupArtifactsRemoved(
|
|
3455
|
+
scope,
|
|
3456
|
+
tombstone,
|
|
3457
|
+
target,
|
|
3458
|
+
pending?.artifactsRemovedAttempt ??
|
|
3459
|
+
active.artifactsRemovedAttempt ??
|
|
3460
|
+
pending?.attempt ??
|
|
3461
|
+
active.attempt,
|
|
3462
|
+
)
|
|
3114
3463
|
? { artifactsRemoved: true as const }
|
|
3115
3464
|
: {}),
|
|
3116
3465
|
});
|
|
3117
3466
|
if (deletion.kind === "artifacts_removed") {
|
|
3118
|
-
if (!cleanupRootsAbsent(tombstone, target, active, false)) continue;
|
|
3119
|
-
if (!cleanupTranscriptParentMatches(target, active)) continue;
|
|
3120
3467
|
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3129
|
-
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3130
|
-
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3131
|
-
|
|
3132
|
-
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3133
|
-
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3134
|
-
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3135
|
-
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3136
|
-
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3137
|
-
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
3138
|
-
artifactsRemoved: true,
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
if (deletion.kind === "cleanup_pending") {
|
|
3142
|
-
const retry = nextCleanupReceipt(target, active);
|
|
3143
|
-
await publishCleanupPending(
|
|
3144
|
-
scope,
|
|
3145
|
-
tombstone,
|
|
3468
|
+
active = { ...active, artifactsRemovedAttempt: active.attempt };
|
|
3469
|
+
const refreshedTarget = validateCandidateForScope(scope, target);
|
|
3470
|
+
if (!refreshedTarget) throw new Error("source_changed");
|
|
3471
|
+
deletion = await deleteSessionVerifiedWithFence(
|
|
3472
|
+
"reconcile",
|
|
3473
|
+
"transcript-after-artifacts-removed",
|
|
3474
|
+
lock,
|
|
3146
3475
|
{
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
detachedArtifactsPath:
|
|
3155
|
-
deletion.phase === "artifacts"
|
|
3156
|
-
? deletion.detachedArtifactsPath
|
|
3157
|
-
: active.detachedArtifactsPath,
|
|
3476
|
+
sessionsRoot: scope.sessionsRoot,
|
|
3477
|
+
transcriptPath: target.path,
|
|
3478
|
+
sessionId: target.sessionId,
|
|
3479
|
+
cwd: target.cwd,
|
|
3480
|
+
transcriptIdentity: refreshedTarget.identity,
|
|
3481
|
+
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3482
|
+
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3158
3483
|
detachedTranscriptPath:
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
? deletion.retainedPlaceholderPath
|
|
3169
|
-
: active.retainedArtifactsPlaceholderPath,
|
|
3170
|
-
retainedArtifactsUnknownPath:
|
|
3171
|
-
deletion.phase === "artifacts"
|
|
3172
|
-
? deletion.retainedUnknownPath
|
|
3173
|
-
: active.retainedArtifactsUnknownPath,
|
|
3174
|
-
retainedTranscriptSuccessorPath:
|
|
3175
|
-
deletion.phase === "transcript"
|
|
3176
|
-
? deletion.retainedSuccessorPath
|
|
3177
|
-
: active.retainedTranscriptSuccessorPath,
|
|
3178
|
-
retainedTranscriptPlaceholderPath:
|
|
3179
|
-
deletion.phase === "transcript"
|
|
3180
|
-
? deletion.retainedPlaceholderPath
|
|
3181
|
-
: active.retainedTranscriptPlaceholderPath,
|
|
3182
|
-
retainedTranscriptUnknownPath:
|
|
3183
|
-
deletion.phase === "transcript"
|
|
3184
|
-
? deletion.retainedUnknownPath
|
|
3185
|
-
: active.retainedTranscriptUnknownPath,
|
|
3484
|
+
active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3485
|
+
|
|
3486
|
+
retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
|
|
3487
|
+
retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
|
|
3488
|
+
retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
|
|
3489
|
+
retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
|
|
3490
|
+
retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
|
|
3491
|
+
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
3492
|
+
artifactsRemoved: true,
|
|
3186
3493
|
},
|
|
3187
|
-
lock,
|
|
3188
3494
|
);
|
|
3189
|
-
continue;
|
|
3190
3495
|
}
|
|
3191
|
-
if (
|
|
3496
|
+
if (deletion.kind === "cleanup_pending") {
|
|
3497
|
+
assertAuthorizedCleanupPending(target, active, deletion);
|
|
3498
|
+
const retry = nextCleanupReceipt(target, active);
|
|
3499
|
+
let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
|
|
3500
|
+
await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
|
|
3501
|
+
if (deletion.phase === "artifacts") {
|
|
3502
|
+
if (
|
|
3503
|
+
deletion.detachedArtifactsPath === active.plannedArtifactsPath &&
|
|
3504
|
+
!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath)
|
|
3505
|
+
) {
|
|
3506
|
+
({ deletion, pendingEvidence } = await continueDetachedArtifactCleanup(
|
|
3507
|
+
scope,
|
|
3508
|
+
tombstone,
|
|
3509
|
+
target,
|
|
3510
|
+
pendingEvidence,
|
|
3511
|
+
observedPending?.detachedTranscriptPath,
|
|
3512
|
+
lock,
|
|
3513
|
+
"reconcile",
|
|
3514
|
+
));
|
|
3515
|
+
}
|
|
3516
|
+
if (
|
|
3517
|
+
deletion.kind === "cleanup_pending" &&
|
|
3518
|
+
deletion.phase === "artifacts" &&
|
|
3519
|
+
(deletion.artifactsPayloadDurable !== true ||
|
|
3520
|
+
!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
|
|
3521
|
+
)
|
|
3522
|
+
continue;
|
|
3523
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
|
|
3524
|
+
pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
|
|
3525
|
+
const retainedProof = cleanupArtifactsRemovedReceipt(
|
|
3526
|
+
tombstone,
|
|
3527
|
+
target,
|
|
3528
|
+
pendingEvidence.artifactsRemovedAttempt ?? pendingEvidence.attempt,
|
|
3529
|
+
);
|
|
3530
|
+
if (!retainedProof) throw new Error("durability_failed");
|
|
3531
|
+
deletion = await deleteSessionVerifiedWithFence(
|
|
3532
|
+
"reconcile",
|
|
3533
|
+
"transcript-after-artifacts-removed",
|
|
3534
|
+
lock,
|
|
3535
|
+
{
|
|
3536
|
+
sessionsRoot: scope.sessionsRoot,
|
|
3537
|
+
transcriptPath: target.path,
|
|
3538
|
+
sessionId: target.sessionId,
|
|
3539
|
+
cwd: target.cwd,
|
|
3540
|
+
transcriptIdentity: {
|
|
3541
|
+
...target.identity,
|
|
3542
|
+
nlink: fs.lstatSync(target.path, { bigint: true }).nlink,
|
|
3543
|
+
},
|
|
3544
|
+
plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
|
|
3545
|
+
plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
|
|
3546
|
+
detachedTranscriptPath:
|
|
3547
|
+
pendingEvidence.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3548
|
+
expectedArtifactsIdentity: retainedProof.identity,
|
|
3549
|
+
expectedArtifactsTree: retainedProof.tree,
|
|
3550
|
+
detachedArtifactsPath: retainedProof.path,
|
|
3551
|
+
retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
|
|
3552
|
+
retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
|
|
3553
|
+
retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
|
|
3554
|
+
retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
|
|
3555
|
+
retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
|
|
3556
|
+
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
3557
|
+
artifactsRemoved: true,
|
|
3558
|
+
},
|
|
3559
|
+
() => {
|
|
3560
|
+
if (
|
|
3561
|
+
!cleanupArtifactsRemoved(
|
|
3562
|
+
scope,
|
|
3563
|
+
tombstone,
|
|
3564
|
+
target,
|
|
3565
|
+
pendingEvidence.artifactsRemovedAttempt ?? pendingEvidence.attempt,
|
|
3566
|
+
)
|
|
3567
|
+
)
|
|
3568
|
+
throw new Error("durability_failed");
|
|
3569
|
+
},
|
|
3570
|
+
);
|
|
3571
|
+
if (deletion.kind === "cleanup_pending") {
|
|
3572
|
+
if (
|
|
3573
|
+
deletion.phase !== "transcript" ||
|
|
3574
|
+
(deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath &&
|
|
3575
|
+
deletion.transcriptPayloadDurable !== true)
|
|
3576
|
+
)
|
|
3577
|
+
throw new Error("durability_failed");
|
|
3578
|
+
const followup = nextCleanupReceipt(target, pendingEvidence);
|
|
3579
|
+
pendingEvidence = cleanupPendingEvidence(followup, pendingEvidence, deletion);
|
|
3580
|
+
await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
|
|
3581
|
+
}
|
|
3582
|
+
if (deletion.kind === "deleted" && fs.existsSync(pendingEvidence.plannedTranscriptPath)) {
|
|
3583
|
+
if (!reconcileScrubbedTranscriptPlaceholder(pendingEvidence.plannedTranscriptPath))
|
|
3584
|
+
throw new Error("durability_failed");
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
if (
|
|
3588
|
+
deletion.kind === "cleanup_pending" &&
|
|
3589
|
+
(deletion.phase !== "transcript" ||
|
|
3590
|
+
deletion.transcriptPayloadDurable !== true ||
|
|
3591
|
+
!cleanupRootsAbsent(
|
|
3592
|
+
tombstone,
|
|
3593
|
+
target,
|
|
3594
|
+
pendingEvidence,
|
|
3595
|
+
pendingEvidence.artifactsPayloadDurable === true &&
|
|
3596
|
+
pendingEvidence.detachedArtifactsPath &&
|
|
3597
|
+
retainedArtifactPayloadAbsent(pendingEvidence.detachedArtifactsPath)
|
|
3598
|
+
? pendingEvidence.detachedArtifactsPath
|
|
3599
|
+
: undefined,
|
|
3600
|
+
))
|
|
3601
|
+
)
|
|
3602
|
+
continue;
|
|
3603
|
+
}
|
|
3192
3604
|
fsyncManagedParent(target.path);
|
|
3193
3605
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
3194
3606
|
} catch (error) {
|
|
@@ -3201,7 +3613,7 @@ export async function reconcileManagedTombstones(
|
|
|
3201
3613
|
}
|
|
3202
3614
|
}
|
|
3203
3615
|
} finally {
|
|
3204
|
-
if (lock) await lock.release()
|
|
3616
|
+
if (lock) await lock.release();
|
|
3205
3617
|
}
|
|
3206
3618
|
}
|
|
3207
3619
|
}
|
|
@@ -3230,31 +3642,27 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
3230
3642
|
} catch (error) {
|
|
3231
3643
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
3232
3644
|
const message =
|
|
3233
|
-
publication?.classification ??
|
|
3234
|
-
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
3645
|
+
publication?.classification ?? managedScopeFailureMessage(error, "Managed write protocol setup failed.");
|
|
3235
3646
|
const code =
|
|
3236
3647
|
message === "atomic_unavailable" ||
|
|
3237
3648
|
message === "invalid_request" ||
|
|
3238
3649
|
message === "durability_failed" ||
|
|
3239
|
-
message === "durability_not_provable"
|
|
3650
|
+
message === "durability_not_provable" ||
|
|
3651
|
+
message === "migration_busy"
|
|
3240
3652
|
? message
|
|
3241
3653
|
: "binding_invalid";
|
|
3242
3654
|
return {
|
|
3243
3655
|
kind: "error",
|
|
3244
3656
|
code,
|
|
3245
3657
|
message,
|
|
3246
|
-
|
|
3247
|
-
? {
|
|
3248
|
-
:
|
|
3658
|
+
cause: publication
|
|
3659
|
+
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
3660
|
+
: managedScopeFailureCause(error),
|
|
3249
3661
|
};
|
|
3250
3662
|
}
|
|
3251
3663
|
}
|
|
3252
3664
|
|
|
3253
|
-
|
|
3254
|
-
* Open a validated candidate for mutation. Legacy transcripts are copied exactly once
|
|
3255
|
-
* into v2 and retained at their original location; no transcript data is merged.
|
|
3256
|
-
*/
|
|
3257
|
-
export async function openManagedCandidateForWrite(
|
|
3665
|
+
async function openManagedCandidateForWriteInternal(
|
|
3258
3666
|
scope: ManagedScope,
|
|
3259
3667
|
candidate: ManagedCandidate,
|
|
3260
3668
|
expectedIdentityOrMigrationPolicy: ResumeSessionIdentity | ManagedMigrationPolicy = "copy-retain",
|
|
@@ -3575,12 +3983,43 @@ export async function openManagedCandidateForWrite(
|
|
|
3575
3983
|
: "Managed migration failed.",
|
|
3576
3984
|
};
|
|
3577
3985
|
} finally {
|
|
3578
|
-
if (lock)
|
|
3986
|
+
if (lock) {
|
|
3987
|
+
await ManagedSessionScopeTestHooks.beforeManagedLockRelease?.({ path: lock.path, attemptId: lock.attemptId });
|
|
3988
|
+
await lock.release();
|
|
3989
|
+
}
|
|
3579
3990
|
}
|
|
3580
3991
|
}
|
|
3581
3992
|
|
|
3582
|
-
/**
|
|
3583
|
-
|
|
3993
|
+
/**
|
|
3994
|
+
* Open a validated candidate for mutation. Legacy transcripts are copied exactly once
|
|
3995
|
+
* into v2 and retained at their original location; no transcript data is merged.
|
|
3996
|
+
*/
|
|
3997
|
+
export async function openManagedCandidateForWrite(
|
|
3998
|
+
scope: ManagedScope,
|
|
3999
|
+
candidate: ManagedCandidate,
|
|
4000
|
+
expectedIdentityOrMigrationPolicy: ResumeSessionIdentity | ManagedMigrationPolicy = "copy-retain",
|
|
4001
|
+
migrationPolicy: ManagedMigrationPolicy = typeof expectedIdentityOrMigrationPolicy === "string"
|
|
4002
|
+
? expectedIdentityOrMigrationPolicy
|
|
4003
|
+
: "copy-retain",
|
|
4004
|
+
authority?: ManagedCandidateWriteAuthority,
|
|
4005
|
+
): Promise<ManagedOpenCandidateResult> {
|
|
4006
|
+
try {
|
|
4007
|
+
return await openManagedCandidateForWriteInternal(
|
|
4008
|
+
scope,
|
|
4009
|
+
candidate,
|
|
4010
|
+
expectedIdentityOrMigrationPolicy,
|
|
4011
|
+
migrationPolicy,
|
|
4012
|
+
authority,
|
|
4013
|
+
);
|
|
4014
|
+
} catch (error) {
|
|
4015
|
+
const code = expectedFailure(error);
|
|
4016
|
+
if (code === "migration_busy")
|
|
4017
|
+
return { kind: "error", code, message: error instanceof Error ? error.message : "migration_busy" };
|
|
4018
|
+
throw error;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
async function deleteManagedSessionCandidateInternal(
|
|
3584
4023
|
scope: ManagedScope,
|
|
3585
4024
|
candidate: ManagedCandidate,
|
|
3586
4025
|
): Promise<ManagedDeleteCandidateResult> {
|
|
@@ -3611,11 +4050,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
3611
4050
|
let targets = retiredTargets(scope, tombstone);
|
|
3612
4051
|
if (!targets) {
|
|
3613
4052
|
if (!current) throw new Error("source_changed");
|
|
3614
|
-
|
|
3615
|
-
authorizeRetiredTarget(scope, target),
|
|
3616
|
-
);
|
|
3617
|
-
if (authorizedTargets.some(target => target === undefined)) throw new Error("source_changed");
|
|
3618
|
-
targets = authorizedTargets as RetiredTarget[];
|
|
4053
|
+
targets = [current, ...(paired ? [paired] : [])];
|
|
3619
4054
|
lock.assertOwned();
|
|
3620
4055
|
try {
|
|
3621
4056
|
await publishManagedTombstone(
|
|
@@ -3630,10 +4065,6 @@ export async function deleteManagedSessionCandidate(
|
|
|
3630
4065
|
cwd: target.cwd,
|
|
3631
4066
|
provenance: target.provenance,
|
|
3632
4067
|
identity: target.identity,
|
|
3633
|
-
transcriptParentIdentity: {
|
|
3634
|
-
dev: target.transcriptParentIdentity.dev.toString(),
|
|
3635
|
-
ino: target.transcriptParentIdentity.ino.toString(),
|
|
3636
|
-
},
|
|
3637
4068
|
})),
|
|
3638
4069
|
},
|
|
3639
4070
|
lock.assertOwned,
|
|
@@ -3649,25 +4080,28 @@ export async function deleteManagedSessionCandidate(
|
|
|
3649
4080
|
lock.assertOwned();
|
|
3650
4081
|
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
3651
4082
|
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
3652
|
-
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
3653
4083
|
try {
|
|
3654
4084
|
fs.lstatSync(target.path);
|
|
3655
4085
|
} catch (error) {
|
|
3656
4086
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
3657
4087
|
if (!observedPending) continue;
|
|
4088
|
+
const artifactsEvidence = cleanupArtifactsRemovedEvidence(
|
|
4089
|
+
scope,
|
|
4090
|
+
tombstone,
|
|
4091
|
+
target,
|
|
4092
|
+
observedPending.artifactsRemovedAttempt ?? observedPending.attempt,
|
|
4093
|
+
);
|
|
3658
4094
|
if (
|
|
3659
|
-
|
|
3660
|
-
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
3661
|
-
)
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
message:
|
|
3667
|
-
"Exact cleanup remains pending because transcript authority disappeared without native deletion proof.",
|
|
3668
|
-
};
|
|
4095
|
+
artifactsEvidence &&
|
|
4096
|
+
cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath)
|
|
4097
|
+
) {
|
|
4098
|
+
fsyncManagedParent(target.path);
|
|
4099
|
+
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
4100
|
+
continue;
|
|
4101
|
+
}
|
|
3669
4102
|
} else throw error;
|
|
3670
4103
|
}
|
|
4104
|
+
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
3671
4105
|
deletedAny = true;
|
|
3672
4106
|
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
3673
4107
|
if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
|
|
@@ -3675,30 +4109,25 @@ export async function deleteManagedSessionCandidate(
|
|
|
3675
4109
|
!!observedPending &&
|
|
3676
4110
|
(observedPending.detachedArtifactsPath !== pending?.detachedArtifactsPath ||
|
|
3677
4111
|
observedPending.detachedTranscriptPath !== pending?.detachedTranscriptPath);
|
|
3678
|
-
|
|
4112
|
+
let active =
|
|
3679
4113
|
discoveredDetach || (observedPending && requiresFreshCleanupPlan(observedPending))
|
|
3680
4114
|
? nextCleanupReceipt(target, observedPending)
|
|
3681
4115
|
: (observedPending ?? nextCleanupReceipt(target, undefined));
|
|
3682
4116
|
if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
|
|
3683
4117
|
await publishCleanupPending(scope, tombstone, active, lock);
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
tombstonePath: tombstone,
|
|
3688
|
-
phase: cleanupArtifactsRemoved(scope, tombstone, target, pending?.attempt ?? active.attempt)
|
|
3689
|
-
? "transcript"
|
|
3690
|
-
: "artifacts",
|
|
3691
|
-
message: "Exact cleanup remains pending because transcript parent identity changed before mutation.",
|
|
3692
|
-
};
|
|
3693
|
-
let deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
4118
|
+
const initialTarget = validateCandidateForScope(scope, target);
|
|
4119
|
+
if (!initialTarget) throw new Error("source_changed");
|
|
4120
|
+
let deletion = await deleteSessionVerifiedWithFence("direct", "initial", lock, {
|
|
3694
4121
|
sessionsRoot: scope.sessionsRoot,
|
|
3695
4122
|
transcriptPath: target.path,
|
|
3696
4123
|
sessionId: target.sessionId,
|
|
3697
4124
|
cwd: target.cwd,
|
|
3698
|
-
transcriptIdentity:
|
|
3699
|
-
transcriptParentIdentity:
|
|
4125
|
+
transcriptIdentity: initialTarget.identity,
|
|
4126
|
+
transcriptParentIdentity: (() => {
|
|
4127
|
+
const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
4128
|
+
return { dev: parent.dev, ino: parent.ino };
|
|
4129
|
+
})(),
|
|
3700
4130
|
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
3701
|
-
artifactsAbsentAtAuthorization: active.artifactsAbsentAtAuthorization,
|
|
3702
4131
|
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
3703
4132
|
detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
|
|
3704
4133
|
detachedTranscriptPath:
|
|
@@ -3713,33 +4142,26 @@ export async function deleteManagedSessionCandidate(
|
|
|
3713
4142
|
retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
|
|
3714
4143
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3715
4144
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3716
|
-
...(cleanupArtifactsRemoved(
|
|
4145
|
+
...(cleanupArtifactsRemoved(
|
|
4146
|
+
scope,
|
|
4147
|
+
tombstone,
|
|
4148
|
+
target,
|
|
4149
|
+
pending?.artifactsRemovedAttempt ?? active.artifactsRemovedAttempt ?? pending?.attempt ?? active.attempt,
|
|
4150
|
+
)
|
|
3717
4151
|
? { artifactsRemoved: true as const }
|
|
3718
4152
|
: {}),
|
|
3719
4153
|
});
|
|
3720
4154
|
if (deletion.kind === "artifacts_removed") {
|
|
3721
|
-
if (!cleanupRootsAbsent(tombstone, target, active, false))
|
|
3722
|
-
return {
|
|
3723
|
-
kind: "cleanup_pending",
|
|
3724
|
-
tombstonePath: tombstone,
|
|
3725
|
-
phase: "artifacts",
|
|
3726
|
-
message: "Exact cleanup remains pending because artifact or side-path authority is retained.",
|
|
3727
|
-
};
|
|
3728
|
-
if (!cleanupTranscriptParentMatches(target, active))
|
|
3729
|
-
return {
|
|
3730
|
-
kind: "cleanup_pending",
|
|
3731
|
-
tombstonePath: tombstone,
|
|
3732
|
-
phase: "transcript",
|
|
3733
|
-
message: "Exact cleanup remains pending because transcript parent identity changed before mutation.",
|
|
3734
|
-
};
|
|
3735
4155
|
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
3736
|
-
|
|
4156
|
+
active = { ...active, artifactsRemovedAttempt: active.attempt };
|
|
4157
|
+
const refreshedTarget = validateCandidateForScope(scope, target);
|
|
4158
|
+
if (!refreshedTarget) throw new Error("source_changed");
|
|
4159
|
+
deletion = await deleteSessionVerifiedWithFence("direct", "transcript-after-artifacts-removed", lock, {
|
|
3737
4160
|
sessionsRoot: scope.sessionsRoot,
|
|
3738
4161
|
transcriptPath: target.path,
|
|
3739
4162
|
sessionId: target.sessionId,
|
|
3740
4163
|
cwd: target.cwd,
|
|
3741
|
-
transcriptIdentity:
|
|
3742
|
-
transcriptParentIdentity: target.transcriptParentIdentity,
|
|
4164
|
+
transcriptIdentity: refreshedTarget.identity,
|
|
3743
4165
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3744
4166
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3745
4167
|
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
@@ -3753,32 +4175,123 @@ export async function deleteManagedSessionCandidate(
|
|
|
3753
4175
|
});
|
|
3754
4176
|
}
|
|
3755
4177
|
if (deletion.kind === "cleanup_pending") {
|
|
4178
|
+
assertAuthorizedCleanupPending(target, active, deletion);
|
|
4179
|
+
const retry = nextCleanupReceipt(target, active);
|
|
4180
|
+
let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
|
|
4181
|
+
await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
|
|
4182
|
+
if (deletion.phase === "artifacts") {
|
|
4183
|
+
if (
|
|
4184
|
+
(deletion.artifactsPayloadDurable === true &&
|
|
4185
|
+
retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath)) ||
|
|
4186
|
+
(deletion.detachedArtifactsPath === active.plannedArtifactsPath &&
|
|
4187
|
+
!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
|
|
4188
|
+
) {
|
|
4189
|
+
({ deletion, pendingEvidence } = await continueDetachedArtifactCleanup(
|
|
4190
|
+
scope,
|
|
4191
|
+
tombstone,
|
|
4192
|
+
target,
|
|
4193
|
+
pendingEvidence,
|
|
4194
|
+
observedPending?.detachedTranscriptPath,
|
|
4195
|
+
lock,
|
|
4196
|
+
"direct",
|
|
4197
|
+
));
|
|
4198
|
+
}
|
|
4199
|
+
if (
|
|
4200
|
+
deletion.kind === "cleanup_pending" &&
|
|
4201
|
+
deletion.phase === "artifacts" &&
|
|
4202
|
+
(deletion.artifactsPayloadDurable !== true ||
|
|
4203
|
+
!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
|
|
4204
|
+
)
|
|
4205
|
+
return {
|
|
4206
|
+
kind: "cleanup_pending",
|
|
4207
|
+
tombstonePath: tombstone,
|
|
4208
|
+
phase: deletion.phase,
|
|
4209
|
+
message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
4210
|
+
};
|
|
4211
|
+
await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
|
|
4212
|
+
pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
|
|
4213
|
+
const retainedProof = cleanupArtifactsRemovedReceipt(
|
|
4214
|
+
tombstone,
|
|
4215
|
+
target,
|
|
4216
|
+
pendingEvidence.artifactsRemovedAttempt ?? pendingEvidence.attempt,
|
|
4217
|
+
);
|
|
4218
|
+
if (!retainedProof) throw new Error("durability_failed");
|
|
4219
|
+
deletion = await deleteSessionVerifiedWithFence(
|
|
4220
|
+
"direct",
|
|
4221
|
+
"transcript-after-artifacts-removed",
|
|
4222
|
+
lock,
|
|
4223
|
+
{
|
|
4224
|
+
sessionsRoot: scope.sessionsRoot,
|
|
4225
|
+
transcriptPath: target.path,
|
|
4226
|
+
sessionId: target.sessionId,
|
|
4227
|
+
cwd: target.cwd,
|
|
4228
|
+
transcriptIdentity: {
|
|
4229
|
+
...target.identity,
|
|
4230
|
+
nlink: fs.lstatSync(target.path, { bigint: true }).nlink,
|
|
4231
|
+
},
|
|
4232
|
+
plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
|
|
4233
|
+
plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
|
|
4234
|
+
detachedTranscriptPath:
|
|
4235
|
+
pendingEvidence.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
4236
|
+
expectedArtifactsIdentity: retainedProof.identity,
|
|
4237
|
+
expectedArtifactsTree: retainedProof.tree,
|
|
4238
|
+
detachedArtifactsPath: retainedProof.path,
|
|
4239
|
+
retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
|
|
4240
|
+
retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
|
|
4241
|
+
retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
|
|
4242
|
+
retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
|
|
4243
|
+
retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
|
|
4244
|
+
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
4245
|
+
artifactsRemoved: true,
|
|
4246
|
+
},
|
|
4247
|
+
() => {
|
|
4248
|
+
if (
|
|
4249
|
+
!cleanupArtifactsRemoved(
|
|
4250
|
+
scope,
|
|
4251
|
+
tombstone,
|
|
4252
|
+
target,
|
|
4253
|
+
pendingEvidence.artifactsRemovedAttempt ?? pendingEvidence.attempt,
|
|
4254
|
+
)
|
|
4255
|
+
)
|
|
4256
|
+
throw new Error("durability_failed");
|
|
4257
|
+
},
|
|
4258
|
+
);
|
|
4259
|
+
if (deletion.kind === "cleanup_pending") {
|
|
4260
|
+
if (
|
|
4261
|
+
deletion.phase !== "transcript" ||
|
|
4262
|
+
(deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath &&
|
|
4263
|
+
deletion.transcriptPayloadDurable !== true)
|
|
4264
|
+
)
|
|
4265
|
+
throw new Error("durability_failed");
|
|
4266
|
+
const followup = nextCleanupReceipt(target, pendingEvidence);
|
|
4267
|
+
pendingEvidence = cleanupPendingEvidence(followup, pendingEvidence, deletion);
|
|
4268
|
+
await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
3756
4271
|
if (
|
|
3757
|
-
|
|
3758
|
-
|
|
4272
|
+
deletion.kind === "cleanup_pending" &&
|
|
4273
|
+
(deletion.phase !== "transcript" ||
|
|
4274
|
+
deletion.transcriptPayloadDurable !== true ||
|
|
4275
|
+
!cleanupRootsAbsent(
|
|
4276
|
+
tombstone,
|
|
3759
4277
|
target,
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
4278
|
+
pendingEvidence,
|
|
4279
|
+
pendingEvidence.artifactsPayloadDurable === true &&
|
|
4280
|
+
pendingEvidence.detachedArtifactsPath &&
|
|
4281
|
+
retainedArtifactPayloadAbsent(pendingEvidence.detachedArtifactsPath)
|
|
4282
|
+
? pendingEvidence.detachedArtifactsPath
|
|
4283
|
+
: undefined,
|
|
4284
|
+
))
|
|
3764
4285
|
)
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
};
|
|
4286
|
+
return {
|
|
4287
|
+
kind: "cleanup_pending",
|
|
4288
|
+
tombstonePath: tombstone,
|
|
4289
|
+
phase: deletion.phase,
|
|
4290
|
+
message: deletion.error.message,
|
|
4291
|
+
};
|
|
4292
|
+
// A retained transcript quarantine proves canonical absence and remains
|
|
4293
|
+
// identity-bound in the durable pending receipt.
|
|
3774
4294
|
}
|
|
3775
|
-
if (!cleanupRootsAbsent(tombstone, target, active))
|
|
3776
|
-
return {
|
|
3777
|
-
kind: "cleanup_pending",
|
|
3778
|
-
tombstonePath: tombstone,
|
|
3779
|
-
phase: "transcript",
|
|
3780
|
-
message: "Exact cleanup remains pending because transcript or artifact authority is retained.",
|
|
3781
|
-
};
|
|
3782
4295
|
fsyncManagedParent(target.path);
|
|
3783
4296
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
3784
4297
|
}
|
|
@@ -3787,6 +4300,24 @@ export async function deleteManagedSessionCandidate(
|
|
|
3787
4300
|
const code = expectedFailure(error);
|
|
3788
4301
|
return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
|
|
3789
4302
|
} finally {
|
|
3790
|
-
if (lock)
|
|
4303
|
+
if (lock) {
|
|
4304
|
+
await ManagedSessionScopeTestHooks.beforeManagedLockRelease?.({ path: lock.path, attemptId: lock.attemptId });
|
|
4305
|
+
await lock.release();
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
/** Tombstone a verified managed candidate before exact-identity deletion. */
|
|
4311
|
+
export async function deleteManagedSessionCandidate(
|
|
4312
|
+
scope: ManagedScope,
|
|
4313
|
+
candidate: ManagedCandidate,
|
|
4314
|
+
): Promise<ManagedDeleteCandidateResult> {
|
|
4315
|
+
try {
|
|
4316
|
+
return await deleteManagedSessionCandidateInternal(scope, candidate);
|
|
4317
|
+
} catch (error) {
|
|
4318
|
+
const code = expectedFailure(error);
|
|
4319
|
+
if (code === "migration_busy")
|
|
4320
|
+
return { kind: "error", code, message: error instanceof Error ? error.message : "migration_busy" };
|
|
4321
|
+
throw error;
|
|
3791
4322
|
}
|
|
3792
4323
|
}
|