@gajae-code/coding-agent 0.12.4 → 0.12.6
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 +36 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +7 -5
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +3 -0
- package/dist/types/task/types.d.ts +194 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +13 -5
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +692 -98
- package/src/sdk/bus/chat-daemon-control.ts +33 -5
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +8 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +332 -198
- package/src/session/internal/managed-session-storage.ts +162 -450
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +31 -28
- package/src/session/session-storage.ts +190 -53
- package/src/session/streaming-output.ts +198 -40
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +277 -64
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +26 -2
- package/src/task/render.ts +42 -1
- package/src/task/types.ts +52 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
- package/src/utils/git.ts +2 -0
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
verifyOwnerOnlyPathSecurity,
|
|
9
9
|
verifyOwnerOnlyPathSecurityExpected,
|
|
10
10
|
} from "@gajae-code/natives";
|
|
11
|
-
import {
|
|
11
|
+
import { logger, pathIsWithin } from "@gajae-code/utils";
|
|
12
12
|
import type { ResumeSessionIdentity } from "../session-manager";
|
|
13
13
|
import {
|
|
14
14
|
FileSessionStorage,
|
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
type ManagedFileSnapshot,
|
|
32
32
|
ManagedPublishError,
|
|
33
33
|
ManagedSessionDescendantStore,
|
|
34
|
-
ManagedSessionSecurityError,
|
|
35
34
|
type ManagedSessionSecurityPolicy,
|
|
36
35
|
type ManagedStorageLock,
|
|
37
36
|
managedSecurityFailureClassification,
|
|
@@ -128,7 +127,7 @@ function configuredRootPath(scope: ManagedScope): string {
|
|
|
128
127
|
const canonical = fs.realpathSync.native(candidate);
|
|
129
128
|
return suffix.length === 0 ? canonical : path.join(canonical, ...suffix);
|
|
130
129
|
} catch (error) {
|
|
131
|
-
if (
|
|
130
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
132
131
|
const parent = path.dirname(candidate);
|
|
133
132
|
if (parent === candidate) throw new Error("Configured managed root is unavailable.");
|
|
134
133
|
suffix.unshift(path.basename(candidate));
|
|
@@ -178,23 +177,6 @@ export type ManagedScopeResolution =
|
|
|
178
177
|
cause?: { readonly classification: string; readonly diagnostic?: string };
|
|
179
178
|
};
|
|
180
179
|
|
|
181
|
-
function managedScopeFailureCause(error: unknown): { readonly classification: string } {
|
|
182
|
-
return { classification: managedSecurityFailureClassification(error) ?? "binding_invalid" };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const managedScopeFailureCodes = new Set([
|
|
186
|
-
"atomic_unavailable",
|
|
187
|
-
"invalid_request",
|
|
188
|
-
"durability_failed",
|
|
189
|
-
"durability_not_provable",
|
|
190
|
-
]);
|
|
191
|
-
|
|
192
|
-
function managedScopeFailureMessage(error: unknown, fallback: string): string {
|
|
193
|
-
const classification = managedSecurityFailureClassification(error);
|
|
194
|
-
if (classification) return classification;
|
|
195
|
-
return error instanceof Error && managedScopeFailureCodes.has(error.message) ? error.message : fallback;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
180
|
export interface ManagedCandidate {
|
|
199
181
|
sessionId: string;
|
|
200
182
|
path: string;
|
|
@@ -295,7 +277,7 @@ function identityFor(cwd: string): NativeIdentity {
|
|
|
295
277
|
function verifyExistingManagedScopeDirectory(pathname: string) {
|
|
296
278
|
if (process.platform !== "win32") return verifyOwnerOnlyPathSecurity(pathname, "directory");
|
|
297
279
|
const expected = fs.lstatSync(pathname, { bigint: true });
|
|
298
|
-
if (!expected.isDirectory() || expected.isSymbolicLink()) throw new
|
|
280
|
+
if (!expected.isDirectory() || expected.isSymbolicLink()) throw new Error("Unsafe managed directory");
|
|
299
281
|
const verified = verifyOwnerOnlyPathSecurityExpected(pathname, "directory", expected.dev, expected.ino);
|
|
300
282
|
const current = fs.lstatSync(pathname, { bigint: true });
|
|
301
283
|
if (
|
|
@@ -304,7 +286,7 @@ function verifyExistingManagedScopeDirectory(pathname: string) {
|
|
|
304
286
|
current.dev !== expected.dev ||
|
|
305
287
|
current.ino !== expected.ino
|
|
306
288
|
)
|
|
307
|
-
throw new
|
|
289
|
+
throw new Error("Managed session directory changed");
|
|
308
290
|
return verified;
|
|
309
291
|
}
|
|
310
292
|
|
|
@@ -423,18 +405,8 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
|
|
|
423
405
|
try {
|
|
424
406
|
raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
|
|
425
407
|
} catch (error) {
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
? "EACCES"
|
|
429
|
-
: hasFsCode(error, "EPERM")
|
|
430
|
-
? "EPERM"
|
|
431
|
-
: "binding_invalid";
|
|
432
|
-
return {
|
|
433
|
-
kind: "error",
|
|
434
|
-
code: "binding_invalid",
|
|
435
|
-
message: "The managed scope binding is invalid JSON.",
|
|
436
|
-
cause: { classification },
|
|
437
|
-
};
|
|
408
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
409
|
+
return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
|
|
438
410
|
}
|
|
439
411
|
return validateBindingRaw(scope, raw);
|
|
440
412
|
}
|
|
@@ -457,16 +429,14 @@ function resolveManagedScopeInternal(
|
|
|
457
429
|
kind: "error",
|
|
458
430
|
code: "sessions_root_unavailable",
|
|
459
431
|
message: "The sessions root is not a safe directory.",
|
|
460
|
-
cause: { classification: "reparse_point" },
|
|
461
432
|
};
|
|
462
433
|
}
|
|
463
434
|
} catch (error) {
|
|
464
|
-
if (
|
|
435
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
465
436
|
return {
|
|
466
437
|
kind: "error",
|
|
467
438
|
code: "sessions_root_unavailable",
|
|
468
439
|
message: "The sessions root could not be inspected.",
|
|
469
|
-
cause: managedScopeFailureCause(error),
|
|
470
440
|
};
|
|
471
441
|
}
|
|
472
442
|
}
|
|
@@ -492,28 +462,21 @@ function resolveManagedScopeInternal(
|
|
|
492
462
|
kind: "error",
|
|
493
463
|
code: "sessions_root_unavailable",
|
|
494
464
|
message: "The sessions root is not a safe directory.",
|
|
495
|
-
cause: { classification: "reparse_point" },
|
|
496
465
|
};
|
|
497
466
|
}
|
|
498
467
|
} catch (error) {
|
|
499
|
-
if (
|
|
468
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
500
469
|
return {
|
|
501
470
|
kind: "error",
|
|
502
471
|
code: "sessions_root_unavailable",
|
|
503
472
|
message: "The sessions root could not be inspected.",
|
|
504
|
-
cause: managedScopeFailureCause(error),
|
|
505
473
|
};
|
|
506
474
|
}
|
|
507
475
|
}
|
|
508
476
|
try {
|
|
509
477
|
const directory = fs.lstatSync(scope.directoryPath);
|
|
510
478
|
if (!directory.isDirectory() || directory.isSymbolicLink()) {
|
|
511
|
-
return {
|
|
512
|
-
kind: "error",
|
|
513
|
-
code: "binding_invalid",
|
|
514
|
-
message: "The managed scope path is not a safe directory.",
|
|
515
|
-
cause: { classification: "reparse_point" },
|
|
516
|
-
};
|
|
479
|
+
return { kind: "error", code: "binding_invalid", message: "The managed scope path is not a safe directory." };
|
|
517
480
|
}
|
|
518
481
|
const security = validateNativeSecurityResult(
|
|
519
482
|
verifyExistingManagedScopeDirectory(scope.directoryPath),
|
|
@@ -525,17 +488,11 @@ function resolveManagedScopeInternal(
|
|
|
525
488
|
kind: "error",
|
|
526
489
|
code: "binding_invalid",
|
|
527
490
|
message: "The managed scope security could not be verified.",
|
|
528
|
-
cause: { classification: security.code ?? "acl_verify_failed" },
|
|
529
491
|
};
|
|
530
492
|
}
|
|
531
493
|
} catch (error) {
|
|
532
|
-
if (
|
|
533
|
-
return {
|
|
534
|
-
kind: "error",
|
|
535
|
-
code: "binding_invalid",
|
|
536
|
-
message: "The managed scope path could not be inspected.",
|
|
537
|
-
cause: managedScopeFailureCause(error),
|
|
538
|
-
};
|
|
494
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
495
|
+
return { kind: "error", code: "binding_invalid", message: "The managed scope path could not be inspected." };
|
|
539
496
|
}
|
|
540
497
|
}
|
|
541
498
|
return validateExistingBinding(scope) ?? { kind: "resolved", scope };
|
|
@@ -600,7 +557,7 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
600
557
|
try {
|
|
601
558
|
descriptor = fs.openSync(parent, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
|
|
602
559
|
} catch (error) {
|
|
603
|
-
if (
|
|
560
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT" && path.dirname(parent) !== parent) {
|
|
604
561
|
parent = path.dirname(parent);
|
|
605
562
|
continue;
|
|
606
563
|
}
|
|
@@ -735,6 +692,7 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
|
|
|
735
692
|
named.isSymbolicLink() ||
|
|
736
693
|
named.dev !== snapshot.identity.dev ||
|
|
737
694
|
named.ino !== snapshot.identity.ino ||
|
|
695
|
+
named.nlink !== 1n ||
|
|
738
696
|
Number(named.size) !== snapshot.identity.size ||
|
|
739
697
|
named.mtimeNs !== snapshot.identity.mtimeNs
|
|
740
698
|
)
|
|
@@ -749,6 +707,7 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
|
|
|
749
707
|
canonicalPath: path.resolve(filePath),
|
|
750
708
|
sessionId: header.id,
|
|
751
709
|
...snapshot.identity,
|
|
710
|
+
nlink: named.nlink,
|
|
752
711
|
mtimeMs: Number(named.mtimeMs),
|
|
753
712
|
sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
754
713
|
},
|
|
@@ -822,7 +781,7 @@ export async function ensureManagedScope(
|
|
|
822
781
|
try {
|
|
823
782
|
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
|
|
824
783
|
} catch (error) {
|
|
825
|
-
if (
|
|
784
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
826
785
|
bindingCollision = true;
|
|
827
786
|
}
|
|
828
787
|
const validated = validateExistingBinding(scope);
|
|
@@ -831,14 +790,14 @@ export async function ensureManagedScope(
|
|
|
831
790
|
|
|
832
791
|
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
833
792
|
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
834
|
-
throw new
|
|
793
|
+
throw new Error("Managed session directory changed");
|
|
835
794
|
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
836
795
|
return { kind: "resolved", scope };
|
|
837
796
|
} catch (error) {
|
|
838
797
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
839
798
|
const message =
|
|
840
799
|
publication?.classification ??
|
|
841
|
-
|
|
800
|
+
(error instanceof Error ? error.message : "The managed scope could not be initialized.");
|
|
842
801
|
const code =
|
|
843
802
|
message === "atomic_unavailable" ||
|
|
844
803
|
message === "invalid_request" ||
|
|
@@ -846,13 +805,14 @@ export async function ensureManagedScope(
|
|
|
846
805
|
message === "durability_not_provable"
|
|
847
806
|
? message
|
|
848
807
|
: "binding_invalid";
|
|
808
|
+
|
|
849
809
|
return {
|
|
850
810
|
kind: "error",
|
|
851
811
|
code,
|
|
852
812
|
message,
|
|
853
|
-
|
|
854
|
-
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
855
|
-
:
|
|
813
|
+
...(publication
|
|
814
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
815
|
+
: { cause: { classification: code } }),
|
|
856
816
|
};
|
|
857
817
|
}
|
|
858
818
|
}
|
|
@@ -988,7 +948,7 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
988
948
|
try {
|
|
989
949
|
store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
|
|
990
950
|
} catch (error) {
|
|
991
|
-
if (
|
|
951
|
+
if ((error as Error).message !== "destination_conflict") throw error;
|
|
992
952
|
}
|
|
993
953
|
stage = "binding_read";
|
|
994
954
|
const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
@@ -1026,7 +986,8 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
1026
986
|
} catch (error) {
|
|
1027
987
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
1028
988
|
const message =
|
|
1029
|
-
publication?.classification ??
|
|
989
|
+
publication?.classification ??
|
|
990
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
1030
991
|
const code =
|
|
1031
992
|
message === "atomic_unavailable" ||
|
|
1032
993
|
message === "invalid_request" ||
|
|
@@ -1034,14 +995,14 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
1034
995
|
message === "durability_not_provable"
|
|
1035
996
|
? message
|
|
1036
997
|
: "binding_invalid";
|
|
1037
|
-
|
|
998
|
+
const securityClassification = managedSecurityFailureClassification(error);
|
|
1038
999
|
return {
|
|
1039
1000
|
kind: "error",
|
|
1040
1001
|
code,
|
|
1041
1002
|
message,
|
|
1042
1003
|
cause: publication
|
|
1043
1004
|
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
1044
|
-
: {
|
|
1005
|
+
: { classification: securityClassification ?? code, diagnostic: `prepare:${stage}` },
|
|
1045
1006
|
};
|
|
1046
1007
|
}
|
|
1047
1008
|
}
|
|
@@ -1179,6 +1140,7 @@ function sameCandidate(left: ManagedCandidate, right: ManagedCandidate): boolean
|
|
|
1179
1140
|
left.path === right.path &&
|
|
1180
1141
|
left.identity.dev === right.identity.dev &&
|
|
1181
1142
|
left.identity.ino === right.identity.ino &&
|
|
1143
|
+
left.identity.nlink === right.identity.nlink &&
|
|
1182
1144
|
left.identity.size === right.identity.size &&
|
|
1183
1145
|
left.identity.mtimeNs === right.identity.mtimeNs &&
|
|
1184
1146
|
left.identity.sha256 === right.identity.sha256
|
|
@@ -1191,6 +1153,7 @@ function matchesExpectedResumeIdentity(candidate: ManagedCandidate, expected: Re
|
|
|
1191
1153
|
candidate.identity.sessionId === expected.sessionId &&
|
|
1192
1154
|
candidate.identity.dev === expected.dev &&
|
|
1193
1155
|
candidate.identity.ino === expected.ino &&
|
|
1156
|
+
candidate.identity.nlink === expected.nlink &&
|
|
1194
1157
|
candidate.identity.size === expected.size &&
|
|
1195
1158
|
candidate.identity.mtimeMs === expected.mtimeMs &&
|
|
1196
1159
|
candidate.identity.mtimeNs === expected.mtimeNs &&
|
|
@@ -1405,7 +1368,27 @@ function preparedReceiptMatches(
|
|
|
1405
1368
|
}
|
|
1406
1369
|
}
|
|
1407
1370
|
|
|
1408
|
-
type RetiredTarget = ManagedCandidate;
|
|
1371
|
+
type RetiredTarget = ManagedCandidate & { transcriptParentIdentity: { dev: bigint; ino: bigint } };
|
|
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
|
+
}
|
|
1409
1392
|
|
|
1410
1393
|
function retiredTargets(scope: ManagedScope, pathname: string): readonly RetiredTarget[] | undefined {
|
|
1411
1394
|
try {
|
|
@@ -1426,6 +1409,14 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1426
1409
|
const identity = item.identity;
|
|
1427
1410
|
if (!identity || typeof identity !== "object" || Array.isArray(identity)) return undefined;
|
|
1428
1411
|
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>;
|
|
1429
1420
|
if (
|
|
1430
1421
|
typeof item.path !== "string" ||
|
|
1431
1422
|
typeof item.sessionId !== "string" ||
|
|
@@ -1435,10 +1426,13 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1435
1426
|
typeof fields.canonicalPath !== "string" ||
|
|
1436
1427
|
typeof fields.dev !== "string" ||
|
|
1437
1428
|
typeof fields.ino !== "string" ||
|
|
1429
|
+
typeof fields.nlink !== "string" ||
|
|
1438
1430
|
typeof fields.size !== "number" ||
|
|
1439
1431
|
typeof fields.mtimeMs !== "number" ||
|
|
1440
1432
|
typeof fields.mtimeNs !== "string" ||
|
|
1441
|
-
typeof fields.sha256 !== "string"
|
|
1433
|
+
typeof fields.sha256 !== "string" ||
|
|
1434
|
+
typeof parentFields.dev !== "string" ||
|
|
1435
|
+
typeof parentFields.ino !== "string"
|
|
1442
1436
|
)
|
|
1443
1437
|
return undefined;
|
|
1444
1438
|
const provenance =
|
|
@@ -1457,12 +1451,17 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
|
|
|
1457
1451
|
canonicalPath: fields.canonicalPath,
|
|
1458
1452
|
dev: BigInt(fields.dev),
|
|
1459
1453
|
ino: BigInt(fields.ino),
|
|
1454
|
+
nlink: BigInt(fields.nlink as string),
|
|
1460
1455
|
size: fields.size,
|
|
1461
1456
|
mtimeMs: fields.mtimeMs,
|
|
1462
1457
|
mtimeNs: BigInt(fields.mtimeNs),
|
|
1463
1458
|
sha256: fields.sha256,
|
|
1464
1459
|
sessionId: item.sessionId,
|
|
1465
1460
|
},
|
|
1461
|
+
transcriptParentIdentity: {
|
|
1462
|
+
dev: BigInt(parentFields.dev as string),
|
|
1463
|
+
ino: BigInt(parentFields.ino as string),
|
|
1464
|
+
},
|
|
1466
1465
|
});
|
|
1467
1466
|
}
|
|
1468
1467
|
return targets;
|
|
@@ -1539,7 +1538,9 @@ function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean
|
|
|
1539
1538
|
type CleanupReceipt = {
|
|
1540
1539
|
attempt: number;
|
|
1541
1540
|
target: RetiredTarget;
|
|
1541
|
+
transcriptParentIdentity: { dev: bigint; ino: bigint };
|
|
1542
1542
|
expectedArtifactsIdentity?: SessionStorageFileIdentity;
|
|
1543
|
+
artifactsAbsentAtAuthorization?: true;
|
|
1543
1544
|
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
1544
1545
|
detachedArtifactsPath?: string;
|
|
1545
1546
|
detachedTranscriptPath?: string;
|
|
@@ -1579,7 +1580,12 @@ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: Cleanup
|
|
|
1579
1580
|
cwd: receipt.target.cwd,
|
|
1580
1581
|
identity: receipt.target.identity,
|
|
1581
1582
|
},
|
|
1583
|
+
transcriptParentIdentity: {
|
|
1584
|
+
dev: receipt.transcriptParentIdentity.dev.toString(),
|
|
1585
|
+
ino: receipt.transcriptParentIdentity.ino.toString(),
|
|
1586
|
+
},
|
|
1582
1587
|
...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
|
|
1588
|
+
...(receipt.artifactsAbsentAtAuthorization ? { artifactsAbsentAtAuthorization: true as const } : {}),
|
|
1583
1589
|
...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
|
|
1584
1590
|
...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
|
|
1585
1591
|
...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
|
|
@@ -1711,15 +1717,49 @@ function transcriptRootMatchesTarget(pathname: string, target: RetiredTarget): b
|
|
|
1711
1717
|
}
|
|
1712
1718
|
}
|
|
1713
1719
|
|
|
1714
|
-
function
|
|
1720
|
+
function cleanupTranscriptParentMatches(target: RetiredTarget, pending: CleanupReceipt): boolean {
|
|
1721
|
+
try {
|
|
1722
|
+
const stat = fs.lstatSync(path.dirname(target.path), { bigint: true });
|
|
1723
|
+
return (
|
|
1724
|
+
stat.isDirectory() &&
|
|
1725
|
+
stat.dev === pending.transcriptParentIdentity.dev &&
|
|
1726
|
+
stat.ino === pending.transcriptParentIdentity.ino
|
|
1727
|
+
);
|
|
1728
|
+
} catch {
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function cleanupRootsAbsent(
|
|
1734
|
+
tombstone: string,
|
|
1735
|
+
target: RetiredTarget,
|
|
1736
|
+
pending: CleanupReceipt,
|
|
1737
|
+
includeTranscriptAuthority = true,
|
|
1738
|
+
): 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;
|
|
1715
1753
|
const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
|
|
1716
|
-
const activeTranscriptRoots =
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1754
|
+
const activeTranscriptRoots = includeTranscriptAuthority
|
|
1755
|
+
? new Set(
|
|
1756
|
+
[pending.plannedTranscriptPath, pending.detachedTranscriptPath].filter((pathname): pathname is string =>
|
|
1757
|
+
isQuarantinePath(target, pathname),
|
|
1758
|
+
),
|
|
1759
|
+
)
|
|
1760
|
+
: new Set<string>();
|
|
1721
1761
|
const roots = new Set<string>([
|
|
1722
|
-
target.path,
|
|
1762
|
+
...(includeTranscriptAuthority ? [target.path] : []),
|
|
1723
1763
|
target.path.slice(0, -6),
|
|
1724
1764
|
pending.plannedArtifactsPath,
|
|
1725
1765
|
deterministicRemovalRoot(pending.plannedArtifactsPath),
|
|
@@ -1787,7 +1827,52 @@ function cleanupRootsAbsent(tombstone: string, target: RetiredTarget, pending: C
|
|
|
1787
1827
|
if (!transcriptRootMatchesTarget(pathname, target)) throw new Error("durability_failed");
|
|
1788
1828
|
return false;
|
|
1789
1829
|
}
|
|
1790
|
-
return true;
|
|
1830
|
+
if (!includeTranscriptAuthority) return true;
|
|
1831
|
+
const pendingDirectories = [transcriptParent];
|
|
1832
|
+
const snapshots: Array<{ path: string; stat: fs.BigIntStats }> = [];
|
|
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;
|
|
1875
|
+
}
|
|
1791
1876
|
}
|
|
1792
1877
|
|
|
1793
1878
|
function sameArtifactRootIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
|
|
@@ -1798,20 +1883,21 @@ function sameArtifactTreeSnapshot(left: NativeDirectoryTreeSnapshot, right: Nati
|
|
|
1798
1883
|
if (left.rootDev !== right.rootDev || left.rootIno !== right.rootIno || left.entries.length !== right.entries.length)
|
|
1799
1884
|
return false;
|
|
1800
1885
|
const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string =>
|
|
1801
|
-
JSON.stringify([
|
|
1886
|
+
JSON.stringify([
|
|
1887
|
+
entry.relativePath,
|
|
1888
|
+
entry.kind,
|
|
1889
|
+
entry.dev,
|
|
1890
|
+
entry.ino,
|
|
1891
|
+
entry.nlink,
|
|
1892
|
+
entry.size,
|
|
1893
|
+
entry.mtimeNs,
|
|
1894
|
+
entry.sha256,
|
|
1895
|
+
]);
|
|
1802
1896
|
const leftEntries = left.entries.map(entryKey).sort();
|
|
1803
1897
|
const rightEntries = right.entries.map(entryKey).sort();
|
|
1804
1898
|
return leftEntries.every((entry, index) => entry === rightEntries[index]);
|
|
1805
1899
|
}
|
|
1806
1900
|
|
|
1807
|
-
function artifactTreePayloadAbsent(snapshot: NativeDirectoryTreeSnapshot): boolean {
|
|
1808
|
-
return (
|
|
1809
|
-
snapshot.entries.length === 1 &&
|
|
1810
|
-
snapshot.entries[0]?.relativePath === "" &&
|
|
1811
|
-
snapshot.entries[0].kind === "directory"
|
|
1812
|
-
);
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
1901
|
function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
1816
1902
|
if (!pending.detachedArtifactsPath) return;
|
|
1817
1903
|
if (!pending.expectedArtifactsIdentity || !pending.expectedArtifactsTree) throw new Error("durability_failed");
|
|
@@ -1822,12 +1908,10 @@ function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
|
|
|
1822
1908
|
throw new Error("durability_failed");
|
|
1823
1909
|
}
|
|
1824
1910
|
const observed = artifactIdentityAt(pending.detachedArtifactsPath);
|
|
1825
|
-
const observedTree = snapshotArtifactTree(pending.detachedArtifactsPath);
|
|
1826
1911
|
if (
|
|
1827
1912
|
!observed ||
|
|
1828
1913
|
!sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
|
|
1829
|
-
|
|
1830
|
-
!artifactTreePayloadAbsent(observedTree))
|
|
1914
|
+
!sameArtifactTreeSnapshot(snapshotArtifactTree(pending.detachedArtifactsPath), pending.expectedArtifactsTree)
|
|
1831
1915
|
)
|
|
1832
1916
|
throw new Error("binding_invalid");
|
|
1833
1917
|
}
|
|
@@ -1890,6 +1974,7 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
1890
1974
|
(item.kind === "file" || item.kind === "directory") &&
|
|
1891
1975
|
typeof item.dev === "string" &&
|
|
1892
1976
|
typeof item.ino === "string" &&
|
|
1977
|
+
typeof item.nlink === "string" &&
|
|
1893
1978
|
typeof item.size === "string" &&
|
|
1894
1979
|
typeof item.mtimeNs === "string" &&
|
|
1895
1980
|
typeof item.ctimeNs === "string" &&
|
|
@@ -1937,6 +2022,14 @@ function pendingCleanupReceipt(
|
|
|
1937
2022
|
const attempt = record.attempt;
|
|
1938
2023
|
const recorded = record.target as Record<string, unknown> | undefined;
|
|
1939
2024
|
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");
|
|
1940
2033
|
if (
|
|
1941
2034
|
record.schemaVersion !== 2 ||
|
|
1942
2035
|
record.state !== "cleanup_pending" ||
|
|
@@ -1950,9 +2043,20 @@ function pendingCleanupReceipt(
|
|
|
1950
2043
|
recorded.cwd !== target.cwd ||
|
|
1951
2044
|
identity?.dev !== String(target.identity.dev) ||
|
|
1952
2045
|
identity.ino !== String(target.identity.ino) ||
|
|
2046
|
+
identity.nlink !== String(target.identity.nlink) ||
|
|
1953
2047
|
identity.size !== target.identity.size ||
|
|
1954
2048
|
identity.mtimeNs !== String(target.identity.mtimeNs) ||
|
|
1955
2049
|
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())) ||
|
|
1956
2060
|
!isQuarantinePath(target, record.plannedArtifactsPath) ||
|
|
1957
2061
|
!isQuarantinePath(target, record.plannedTranscriptPath) ||
|
|
1958
2062
|
record.plannedArtifactsPath === record.plannedTranscriptPath ||
|
|
@@ -1974,6 +2078,8 @@ function pendingCleanupReceipt(
|
|
|
1974
2078
|
!isRetainedNativePath(target, record.retainedTranscriptUnknownPath))
|
|
1975
2079
|
)
|
|
1976
2080
|
throw new Error("durability_failed");
|
|
2081
|
+
if (record.artifactsAbsentAtAuthorization !== undefined && record.artifactsAbsentAtAuthorization !== true)
|
|
2082
|
+
throw new Error("durability_failed");
|
|
1977
2083
|
const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
|
|
1978
2084
|
const expectedArtifactsIdentity = artifact
|
|
1979
2085
|
? typeof artifact.dev === "string" &&
|
|
@@ -1991,6 +2097,8 @@ function pendingCleanupReceipt(
|
|
|
1991
2097
|
: undefined
|
|
1992
2098
|
: undefined;
|
|
1993
2099
|
if (artifact && !expectedArtifactsIdentity) throw new Error("durability_failed");
|
|
2100
|
+
if (record.artifactsAbsentAtAuthorization === true && expectedArtifactsIdentity)
|
|
2101
|
+
throw new Error("durability_failed");
|
|
1994
2102
|
const expectedArtifactsTree =
|
|
1995
2103
|
record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
|
|
1996
2104
|
if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
|
|
@@ -2009,7 +2117,12 @@ function pendingCleanupReceipt(
|
|
|
2009
2117
|
latest = {
|
|
2010
2118
|
attempt,
|
|
2011
2119
|
target,
|
|
2120
|
+
transcriptParentIdentity: {
|
|
2121
|
+
dev: BigInt(transcriptParentIdentity.dev as string),
|
|
2122
|
+
ino: BigInt(transcriptParentIdentity.ino as string),
|
|
2123
|
+
},
|
|
2012
2124
|
expectedArtifactsIdentity,
|
|
2125
|
+
artifactsAbsentAtAuthorization: record.artifactsAbsentAtAuthorization === true ? true : undefined,
|
|
2013
2126
|
expectedArtifactsTree,
|
|
2014
2127
|
detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
|
|
2015
2128
|
detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
|
|
@@ -2034,7 +2147,13 @@ function artifactIdentityForCleanup(target: RetiredTarget): SessionStorageFileId
|
|
|
2034
2147
|
try {
|
|
2035
2148
|
const stat = fs.lstatSync(target.path.slice(0, -6), { bigint: true });
|
|
2036
2149
|
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("unsafe_artifacts");
|
|
2037
|
-
return {
|
|
2150
|
+
return {
|
|
2151
|
+
dev: stat.dev,
|
|
2152
|
+
ino: stat.ino,
|
|
2153
|
+
size: Number(stat.size),
|
|
2154
|
+
mtimeNs: stat.mtimeNs,
|
|
2155
|
+
sha256: "",
|
|
2156
|
+
};
|
|
2038
2157
|
} catch (error) {
|
|
2039
2158
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
2040
2159
|
throw error;
|
|
@@ -2053,18 +2172,10 @@ function snapshotArtifactTree(pathname: string): NativeDirectoryTreeSnapshot {
|
|
|
2053
2172
|
return result.snapshot;
|
|
2054
2173
|
}
|
|
2055
2174
|
|
|
2056
|
-
function retainedArtifactPayloadAbsent(pathname: string): boolean {
|
|
2057
|
-
try {
|
|
2058
|
-
return artifactTreePayloadAbsent(snapshotArtifactTree(pathname));
|
|
2059
|
-
} catch (error) {
|
|
2060
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
2061
|
-
throw error;
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
2175
|
function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | undefined): CleanupReceipt {
|
|
2066
2176
|
const attempt = (pending?.attempt ?? 0) + 1;
|
|
2067
2177
|
const directory = path.dirname(target.path);
|
|
2178
|
+
const transcriptParentIdentity = pending?.transcriptParentIdentity ?? target.transcriptParentIdentity;
|
|
2068
2179
|
const operation = stableOperationName(target);
|
|
2069
2180
|
const expectedArtifactsIdentity = pending?.expectedArtifactsIdentity ?? artifactIdentityForCleanup(target);
|
|
2070
2181
|
const expectedArtifactsTree =
|
|
@@ -2074,7 +2185,10 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
|
|
|
2074
2185
|
return {
|
|
2075
2186
|
attempt,
|
|
2076
2187
|
target,
|
|
2188
|
+
transcriptParentIdentity,
|
|
2077
2189
|
expectedArtifactsIdentity,
|
|
2190
|
+
artifactsAbsentAtAuthorization:
|
|
2191
|
+
pending?.artifactsAbsentAtAuthorization ?? (expectedArtifactsIdentity ? undefined : true),
|
|
2078
2192
|
expectedArtifactsTree,
|
|
2079
2193
|
detachedArtifactsPath: pending?.detachedArtifactsPath,
|
|
2080
2194
|
retainedArtifactsSuccessorPath: pending?.retainedArtifactsSuccessorPath,
|
|
@@ -2467,6 +2581,7 @@ function sameArtifactTree(
|
|
|
2467
2581
|
entry.kind,
|
|
2468
2582
|
entry.dev,
|
|
2469
2583
|
entry.ino,
|
|
2584
|
+
entry.nlink,
|
|
2470
2585
|
entry.size,
|
|
2471
2586
|
entry.mtimeNs,
|
|
2472
2587
|
entry.ctimeNs,
|
|
@@ -2949,11 +3064,8 @@ export async function reconcileManagedTombstones(
|
|
|
2949
3064
|
if (
|
|
2950
3065
|
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
2951
3066
|
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
2952
|
-
)
|
|
2953
|
-
fsyncManagedParent(target.path);
|
|
2954
|
-
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
3067
|
+
)
|
|
2955
3068
|
continue;
|
|
2956
|
-
}
|
|
2957
3069
|
} else throw error;
|
|
2958
3070
|
}
|
|
2959
3071
|
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
@@ -2968,13 +3080,16 @@ export async function reconcileManagedTombstones(
|
|
|
2968
3080
|
: (observedPending ?? nextCleanupReceipt(target, undefined));
|
|
2969
3081
|
if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
|
|
2970
3082
|
await publishCleanupPending(scope, tombstone, active, lock);
|
|
3083
|
+
if (!cleanupTranscriptParentMatches(target, active)) continue;
|
|
2971
3084
|
let deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
2972
3085
|
sessionsRoot: scope.sessionsRoot,
|
|
2973
3086
|
transcriptPath: target.path,
|
|
2974
3087
|
sessionId: target.sessionId,
|
|
2975
3088
|
cwd: target.cwd,
|
|
2976
3089
|
transcriptIdentity: target.identity,
|
|
3090
|
+
transcriptParentIdentity: target.transcriptParentIdentity,
|
|
2977
3091
|
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
3092
|
+
artifactsAbsentAtAuthorization: active.artifactsAbsentAtAuthorization,
|
|
2978
3093
|
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
2979
3094
|
detachedArtifactsPath:
|
|
2980
3095
|
active.detachedArtifactsPath ??
|
|
@@ -3000,6 +3115,8 @@ export async function reconcileManagedTombstones(
|
|
|
3000
3115
|
: {}),
|
|
3001
3116
|
});
|
|
3002
3117
|
if (deletion.kind === "artifacts_removed") {
|
|
3118
|
+
if (!cleanupRootsAbsent(tombstone, target, active, false)) continue;
|
|
3119
|
+
if (!cleanupTranscriptParentMatches(target, active)) continue;
|
|
3003
3120
|
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
3004
3121
|
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
3005
3122
|
sessionsRoot: scope.sessionsRoot,
|
|
@@ -3007,6 +3124,7 @@ export async function reconcileManagedTombstones(
|
|
|
3007
3124
|
sessionId: target.sessionId,
|
|
3008
3125
|
cwd: target.cwd,
|
|
3009
3126
|
transcriptIdentity: target.identity,
|
|
3127
|
+
transcriptParentIdentity: target.transcriptParentIdentity,
|
|
3010
3128
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3011
3129
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3012
3130
|
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
@@ -3022,45 +3140,55 @@ export async function reconcileManagedTombstones(
|
|
|
3022
3140
|
}
|
|
3023
3141
|
if (deletion.kind === "cleanup_pending") {
|
|
3024
3142
|
const retry = nextCleanupReceipt(target, active);
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3143
|
+
await publishCleanupPending(
|
|
3144
|
+
scope,
|
|
3145
|
+
tombstone,
|
|
3146
|
+
{
|
|
3147
|
+
...retry,
|
|
3148
|
+
expectedArtifactsIdentity:
|
|
3149
|
+
deletion.phase === "artifacts"
|
|
3150
|
+
? deletion.artifactsIdentity
|
|
3151
|
+
: active.expectedArtifactsIdentity,
|
|
3152
|
+
expectedArtifactsTree:
|
|
3153
|
+
deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
|
|
3154
|
+
detachedArtifactsPath:
|
|
3155
|
+
deletion.phase === "artifacts"
|
|
3156
|
+
? deletion.detachedArtifactsPath
|
|
3157
|
+
: active.detachedArtifactsPath,
|
|
3038
3158
|
detachedTranscriptPath:
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
deletion.
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3159
|
+
deletion.phase === "transcript"
|
|
3160
|
+
? deletion.detachedTranscriptPath
|
|
3161
|
+
: active.detachedTranscriptPath,
|
|
3162
|
+
retainedArtifactsSuccessorPath:
|
|
3163
|
+
deletion.phase === "artifacts"
|
|
3164
|
+
? deletion.retainedSuccessorPath
|
|
3165
|
+
: active.retainedArtifactsSuccessorPath,
|
|
3166
|
+
retainedArtifactsPlaceholderPath:
|
|
3167
|
+
deletion.phase === "artifacts"
|
|
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,
|
|
3186
|
+
},
|
|
3187
|
+
lock,
|
|
3188
|
+
);
|
|
3189
|
+
continue;
|
|
3063
3190
|
}
|
|
3191
|
+
if (!cleanupRootsAbsent(tombstone, target, active)) continue;
|
|
3064
3192
|
fsyncManagedParent(target.path);
|
|
3065
3193
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
3066
3194
|
} catch (error) {
|
|
@@ -3073,7 +3201,7 @@ export async function reconcileManagedTombstones(
|
|
|
3073
3201
|
}
|
|
3074
3202
|
}
|
|
3075
3203
|
} finally {
|
|
3076
|
-
if (lock) await lock.release();
|
|
3204
|
+
if (lock) await lock.release().catch(() => undefined);
|
|
3077
3205
|
}
|
|
3078
3206
|
}
|
|
3079
3207
|
}
|
|
@@ -3102,7 +3230,8 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
3102
3230
|
} catch (error) {
|
|
3103
3231
|
const publication = error instanceof ManagedPublishError ? error : undefined;
|
|
3104
3232
|
const message =
|
|
3105
|
-
publication?.classification ??
|
|
3233
|
+
publication?.classification ??
|
|
3234
|
+
(error instanceof Error ? error.message : "Managed write protocol setup failed.");
|
|
3106
3235
|
const code =
|
|
3107
3236
|
message === "atomic_unavailable" ||
|
|
3108
3237
|
message === "invalid_request" ||
|
|
@@ -3114,9 +3243,9 @@ export async function prepareManagedSessionScopeForWrite(
|
|
|
3114
3243
|
kind: "error",
|
|
3115
3244
|
code,
|
|
3116
3245
|
message,
|
|
3117
|
-
|
|
3118
|
-
? { classification: publication.classification, diagnostic: publication.diagnostic }
|
|
3119
|
-
:
|
|
3246
|
+
...(publication
|
|
3247
|
+
? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
|
|
3248
|
+
: { cause: { classification: code } }),
|
|
3120
3249
|
};
|
|
3121
3250
|
}
|
|
3122
3251
|
}
|
|
@@ -3446,7 +3575,7 @@ export async function openManagedCandidateForWrite(
|
|
|
3446
3575
|
: "Managed migration failed.",
|
|
3447
3576
|
};
|
|
3448
3577
|
} finally {
|
|
3449
|
-
if (lock) await lock.release();
|
|
3578
|
+
if (lock) await lock.release().catch(() => undefined);
|
|
3450
3579
|
}
|
|
3451
3580
|
}
|
|
3452
3581
|
|
|
@@ -3482,7 +3611,11 @@ export async function deleteManagedSessionCandidate(
|
|
|
3482
3611
|
let targets = retiredTargets(scope, tombstone);
|
|
3483
3612
|
if (!targets) {
|
|
3484
3613
|
if (!current) throw new Error("source_changed");
|
|
3485
|
-
|
|
3614
|
+
const authorizedTargets = [current, ...(paired ? [paired] : [])].map(target =>
|
|
3615
|
+
authorizeRetiredTarget(scope, target),
|
|
3616
|
+
);
|
|
3617
|
+
if (authorizedTargets.some(target => target === undefined)) throw new Error("source_changed");
|
|
3618
|
+
targets = authorizedTargets as RetiredTarget[];
|
|
3486
3619
|
lock.assertOwned();
|
|
3487
3620
|
try {
|
|
3488
3621
|
await publishManagedTombstone(
|
|
@@ -3497,6 +3630,10 @@ export async function deleteManagedSessionCandidate(
|
|
|
3497
3630
|
cwd: target.cwd,
|
|
3498
3631
|
provenance: target.provenance,
|
|
3499
3632
|
identity: target.identity,
|
|
3633
|
+
transcriptParentIdentity: {
|
|
3634
|
+
dev: target.transcriptParentIdentity.dev.toString(),
|
|
3635
|
+
ino: target.transcriptParentIdentity.ino.toString(),
|
|
3636
|
+
},
|
|
3500
3637
|
})),
|
|
3501
3638
|
},
|
|
3502
3639
|
lock.assertOwned,
|
|
@@ -3512,6 +3649,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
3512
3649
|
lock.assertOwned();
|
|
3513
3650
|
const pending = pendingCleanupReceipt(scope, tombstone, target);
|
|
3514
3651
|
const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
|
|
3652
|
+
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
3515
3653
|
try {
|
|
3516
3654
|
fs.lstatSync(target.path);
|
|
3517
3655
|
} catch (error) {
|
|
@@ -3520,14 +3658,16 @@ export async function deleteManagedSessionCandidate(
|
|
|
3520
3658
|
if (
|
|
3521
3659
|
cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
|
|
3522
3660
|
cleanupRootsAbsent(tombstone, target, observedPending)
|
|
3523
|
-
)
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3661
|
+
)
|
|
3662
|
+
return {
|
|
3663
|
+
kind: "cleanup_pending",
|
|
3664
|
+
tombstonePath: tombstone,
|
|
3665
|
+
phase: "transcript",
|
|
3666
|
+
message:
|
|
3667
|
+
"Exact cleanup remains pending because transcript authority disappeared without native deletion proof.",
|
|
3668
|
+
};
|
|
3528
3669
|
} else throw error;
|
|
3529
3670
|
}
|
|
3530
|
-
if (cleanupCompleted(scope, tombstone, target)) continue;
|
|
3531
3671
|
deletedAny = true;
|
|
3532
3672
|
const verified = observedPending ? target : validateCandidateForScope(scope, target);
|
|
3533
3673
|
if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
|
|
@@ -3541,13 +3681,24 @@ export async function deleteManagedSessionCandidate(
|
|
|
3541
3681
|
: (observedPending ?? nextCleanupReceipt(target, undefined));
|
|
3542
3682
|
if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
|
|
3543
3683
|
await publishCleanupPending(scope, tombstone, active, lock);
|
|
3684
|
+
if (!cleanupTranscriptParentMatches(target, active))
|
|
3685
|
+
return {
|
|
3686
|
+
kind: "cleanup_pending",
|
|
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
|
+
};
|
|
3544
3693
|
let deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
3545
3694
|
sessionsRoot: scope.sessionsRoot,
|
|
3546
3695
|
transcriptPath: target.path,
|
|
3547
3696
|
sessionId: target.sessionId,
|
|
3548
3697
|
cwd: target.cwd,
|
|
3549
3698
|
transcriptIdentity: target.identity,
|
|
3699
|
+
transcriptParentIdentity: target.transcriptParentIdentity,
|
|
3550
3700
|
expectedArtifactsIdentity: active.expectedArtifactsIdentity,
|
|
3701
|
+
artifactsAbsentAtAuthorization: active.artifactsAbsentAtAuthorization,
|
|
3551
3702
|
expectedArtifactsTree: active.expectedArtifactsTree,
|
|
3552
3703
|
detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
|
|
3553
3704
|
detachedTranscriptPath:
|
|
@@ -3567,6 +3718,20 @@ export async function deleteManagedSessionCandidate(
|
|
|
3567
3718
|
: {}),
|
|
3568
3719
|
});
|
|
3569
3720
|
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
|
+
};
|
|
3570
3735
|
await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
|
|
3571
3736
|
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
3572
3737
|
sessionsRoot: scope.sessionsRoot,
|
|
@@ -3574,6 +3739,7 @@ export async function deleteManagedSessionCandidate(
|
|
|
3574
3739
|
sessionId: target.sessionId,
|
|
3575
3740
|
cwd: target.cwd,
|
|
3576
3741
|
transcriptIdentity: target.identity,
|
|
3742
|
+
transcriptParentIdentity: target.transcriptParentIdentity,
|
|
3577
3743
|
plannedArtifactsPath: active.plannedArtifactsPath,
|
|
3578
3744
|
plannedTranscriptPath: active.plannedTranscriptPath,
|
|
3579
3745
|
detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
@@ -3598,53 +3764,21 @@ export async function deleteManagedSessionCandidate(
|
|
|
3598
3764
|
)
|
|
3599
3765
|
throw new Error("durability_failed");
|
|
3600
3766
|
const retry = nextCleanupReceipt(target, active);
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
phase: deletion.phase,
|
|
3609
|
-
message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
3610
|
-
};
|
|
3611
|
-
await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
|
|
3612
|
-
deletion = await new FileSessionStorage().deleteSessionVerified({
|
|
3613
|
-
sessionsRoot: scope.sessionsRoot,
|
|
3614
|
-
transcriptPath: target.path,
|
|
3615
|
-
sessionId: target.sessionId,
|
|
3616
|
-
cwd: target.cwd,
|
|
3617
|
-
transcriptIdentity: target.identity,
|
|
3618
|
-
plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
|
|
3619
|
-
plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
|
|
3620
|
-
detachedTranscriptPath:
|
|
3621
|
-
pendingEvidence.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
|
|
3622
|
-
retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
|
|
3623
|
-
retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
|
|
3624
|
-
retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
|
|
3625
|
-
retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
|
|
3626
|
-
retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
|
|
3627
|
-
retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
|
|
3628
|
-
artifactsRemoved: true,
|
|
3629
|
-
});
|
|
3630
|
-
if (deletion.kind === "cleanup_pending") {
|
|
3631
|
-
if (
|
|
3632
|
-
deletion.phase !== "transcript" ||
|
|
3633
|
-
deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath
|
|
3634
|
-
)
|
|
3635
|
-
throw new Error("durability_failed");
|
|
3636
|
-
const followup = nextCleanupReceipt(target, pendingEvidence);
|
|
3637
|
-
await publishCleanupPending(
|
|
3638
|
-
scope,
|
|
3639
|
-
tombstone,
|
|
3640
|
-
cleanupPendingEvidence(followup, pendingEvidence, deletion),
|
|
3641
|
-
lock,
|
|
3642
|
-
);
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
// A retained transcript quarantine proves canonical absence and remains
|
|
3646
|
-
// identity-bound in the durable pending receipt.
|
|
3767
|
+
await publishCleanupPending(scope, tombstone, cleanupPendingEvidence(retry, active, deletion), lock);
|
|
3768
|
+
return {
|
|
3769
|
+
kind: "cleanup_pending",
|
|
3770
|
+
tombstonePath: tombstone,
|
|
3771
|
+
phase: deletion.phase,
|
|
3772
|
+
message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
|
|
3773
|
+
};
|
|
3647
3774
|
}
|
|
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
|
+
};
|
|
3648
3782
|
fsyncManagedParent(target.path);
|
|
3649
3783
|
await publishCleanupCompleted(scope, tombstone, target, lock);
|
|
3650
3784
|
}
|
|
@@ -3653,6 +3787,6 @@ export async function deleteManagedSessionCandidate(
|
|
|
3653
3787
|
const code = expectedFailure(error);
|
|
3654
3788
|
return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
|
|
3655
3789
|
} finally {
|
|
3656
|
-
if (lock) await lock.release();
|
|
3790
|
+
if (lock) await lock.release().catch(() => undefined);
|
|
3657
3791
|
}
|
|
3658
3792
|
}
|