@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.
Files changed (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +9 -1
  4. package/dist/types/eval/backend.d.ts +2 -2
  5. package/dist/types/exec/bash-executor.d.ts +10 -8
  6. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  8. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  9. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  10. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  11. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  12. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  15. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  16. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  17. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  19. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  20. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  21. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  23. package/dist/types/sdk/broker/broker.d.ts +18 -0
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  25. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  26. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
  27. package/dist/types/sdk/bus/index.d.ts +4 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  30. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  31. package/dist/types/sdk/host/host.d.ts +23 -2
  32. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  33. package/dist/types/session/agent-session.d.ts +5 -4
  34. package/dist/types/session/artifacts.d.ts +8 -5
  35. package/dist/types/session/attempt-record-store.d.ts +19 -0
  36. package/dist/types/session/client-bridge.d.ts +14 -0
  37. package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
  38. package/dist/types/session/messages.d.ts +4 -1
  39. package/dist/types/session/session-manager.d.ts +7 -5
  40. package/dist/types/session/session-storage.d.ts +8 -0
  41. package/dist/types/session/streaming-output.d.ts +32 -0
  42. package/dist/types/task/executor.d.ts +1 -1
  43. package/dist/types/task/index.d.ts +2 -1
  44. package/dist/types/task/receipt.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +194 -0
  46. package/dist/types/task/worktree.d.ts +13 -6
  47. package/dist/types/tools/bash.d.ts +1 -1
  48. package/dist/types/tools/fetch.d.ts +3 -0
  49. package/dist/types/tools/output-meta.d.ts +37 -5
  50. package/dist/types/tools/subagent.d.ts +2 -0
  51. package/dist/types/tools/tool-result.d.ts +1 -0
  52. package/dist/types/utils/artifact-id.d.ts +2 -0
  53. package/dist/types/utils/git.d.ts +1 -0
  54. package/package.json +8 -8
  55. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  56. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  57. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  58. package/src/async/job-manager.ts +13 -5
  59. package/src/config/model-profiles.ts +8 -0
  60. package/src/config/settings-schema.ts +17 -3
  61. package/src/cursor.ts +20 -13
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  63. package/src/eval/backend.ts +2 -2
  64. package/src/exec/bash-executor.ts +425 -151
  65. package/src/extensibility/extensions/runner.ts +80 -6
  66. package/src/extensibility/extensions/wrapper.ts +22 -15
  67. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  68. package/src/gjc-runtime/launch-tmux.ts +309 -293
  69. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  70. package/src/gjc-runtime/psmux-detect.ts +52 -6
  71. package/src/gjc-runtime/team-launch.ts +50 -2
  72. package/src/gjc-runtime/team-runtime.ts +716 -251
  73. package/src/gjc-runtime/team-store.ts +25 -7
  74. package/src/gjc-runtime/team-workers.ts +161 -1
  75. package/src/gjc-runtime/tmux-common.ts +16 -4
  76. package/src/gjc-runtime/tmux-gc.ts +37 -5
  77. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  78. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  79. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  80. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  82. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  83. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  84. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  85. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  86. package/src/internal-urls/agent-protocol.ts +98 -38
  87. package/src/internal-urls/artifact-protocol.ts +28 -17
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/internal-urls/local-protocol.ts +39 -18
  90. package/src/internal-urls/registry-helpers.ts +62 -0
  91. package/src/modes/acp/acp-agent.ts +51 -13
  92. package/src/modes/acp/acp-client-bridge.ts +154 -11
  93. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  94. package/src/modes/components/plugin-settings.ts +5 -0
  95. package/src/modes/components/settings-selector.ts +4 -0
  96. package/src/modes/controllers/selector-controller.ts +1 -0
  97. package/src/prompts/tools/bash.md +2 -2
  98. package/src/prompts/tools/monitor.md +4 -4
  99. package/src/sdk/acp/adapter.ts +200 -9
  100. package/src/sdk/broker/broker.ts +93 -10
  101. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  102. package/src/sdk/broker/lifecycle.ts +692 -98
  103. package/src/sdk/bus/chat-daemon-control.ts +33 -5
  104. package/src/sdk/bus/index.ts +347 -14
  105. package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
  106. package/src/sdk/bus/telegram-daemon.ts +283 -44
  107. package/src/sdk/bus/topic-registry.ts +133 -10
  108. package/src/sdk/host/host.ts +187 -9
  109. package/src/sdk/host/reverse-leases.ts +249 -42
  110. package/src/sdk/session.ts +8 -6
  111. package/src/session/agent-session.ts +930 -354
  112. package/src/session/artifacts.ts +211 -74
  113. package/src/session/attempt-record-store.ts +106 -0
  114. package/src/session/client-bridge.ts +15 -0
  115. package/src/session/internal/managed-session-scope.ts +332 -198
  116. package/src/session/internal/managed-session-storage.ts +162 -450
  117. package/src/session/messages.ts +32 -7
  118. package/src/session/session-manager.ts +31 -28
  119. package/src/session/session-storage.ts +190 -53
  120. package/src/session/streaming-output.ts +198 -40
  121. package/src/task/executor.ts +5 -2
  122. package/src/task/index.ts +277 -64
  123. package/src/task/provider-retry-status.ts +4 -1
  124. package/src/task/receipt.ts +26 -2
  125. package/src/task/render.ts +42 -1
  126. package/src/task/types.ts +52 -0
  127. package/src/task/worktree.ts +122 -43
  128. package/src/tools/bash.ts +818 -239
  129. package/src/tools/eval.ts +99 -4
  130. package/src/tools/fetch.ts +77 -15
  131. package/src/tools/find.ts +7 -3
  132. package/src/tools/monitor.ts +7 -1
  133. package/src/tools/output-meta.ts +706 -116
  134. package/src/tools/search.ts +9 -4
  135. package/src/tools/subagent-render.ts +5 -2
  136. package/src/tools/subagent.ts +8 -0
  137. package/src/tools/tool-result.ts +1 -0
  138. package/src/utils/artifact-id.ts +6 -0
  139. 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 { hasFsCode, logger, pathIsWithin } from "@gajae-code/utils";
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 (!hasFsCode(error, "ENOENT")) throw error;
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 ManagedSessionSecurityError("reparse_point");
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 ManagedSessionSecurityError("identity_mismatch");
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 (hasFsCode(error, "ENOENT")) return undefined;
427
- const classification = hasFsCode(error, "EACCES")
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 (!hasFsCode(error, "ENOENT")) {
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 (!hasFsCode(error, "ENOENT")) {
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 (!hasFsCode(error, "ENOENT")) {
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 (hasFsCode(error, "ENOENT") && path.dirname(parent) !== parent) {
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 (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
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 ManagedSessionSecurityError("reparse_point");
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
- managedScopeFailureMessage(error, "The managed scope could not be initialized.");
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
- cause: publication
854
- ? { classification: publication.classification, diagnostic: publication.diagnostic }
855
- : managedScopeFailureCause(error),
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 (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
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 ?? managedScopeFailureMessage(error, "Managed write protocol setup failed.");
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
- : { ...managedScopeFailureCause(error), diagnostic: `prepare:${stage}` },
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 cleanupRootsAbsent(tombstone: string, target: RetiredTarget, pending: CleanupReceipt): boolean {
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 = new Set(
1717
- [pending.plannedTranscriptPath, pending.detachedTranscriptPath].filter((pathname): pathname is string =>
1718
- isQuarantinePath(target, pathname),
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([entry.relativePath, entry.kind, entry.dev, entry.ino, entry.size, entry.mtimeNs, entry.sha256]);
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
- (!sameArtifactTreeSnapshot(observedTree, pending.expectedArtifactsTree) &&
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 { dev: stat.dev, ino: stat.ino, size: Number(stat.size), mtimeNs: stat.mtimeNs, sha256: "" };
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
- const pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
3026
- await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
3027
- if (deletion.phase === "artifacts") {
3028
- if (!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath)) continue;
3029
- await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
3030
- deletion = await new FileSessionStorage().deleteSessionVerified({
3031
- sessionsRoot: scope.sessionsRoot,
3032
- transcriptPath: target.path,
3033
- sessionId: target.sessionId,
3034
- cwd: target.cwd,
3035
- transcriptIdentity: target.identity,
3036
- plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
3037
- plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
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
- pendingEvidence.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
3040
- retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
3041
- retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
3042
- retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
3043
- retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
3044
- retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
3045
- retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
3046
- artifactsRemoved: true,
3047
- });
3048
- if (deletion.kind === "cleanup_pending") {
3049
- if (
3050
- deletion.phase !== "transcript" ||
3051
- deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath
3052
- )
3053
- throw new Error("durability_failed");
3054
- const followup = nextCleanupReceipt(target, pendingEvidence);
3055
- await publishCleanupPending(
3056
- scope,
3057
- tombstone,
3058
- cleanupPendingEvidence(followup, pendingEvidence, deletion),
3059
- lock,
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 ?? managedScopeFailureMessage(error, "Managed write protocol setup failed.");
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
- cause: publication
3118
- ? { classification: publication.classification, diagnostic: publication.diagnostic }
3119
- : managedScopeFailureCause(error),
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
- targets = [current, ...(paired ? [paired] : [])];
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
- fsyncManagedParent(target.path);
3525
- await publishCleanupCompleted(scope, tombstone, target, lock);
3526
- continue;
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
- const pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
3602
- await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
3603
- if (deletion.phase === "artifacts") {
3604
- if (!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
3605
- return {
3606
- kind: "cleanup_pending",
3607
- tombstonePath: tombstone,
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
  }