@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
@@ -393,6 +393,7 @@ type CleanupEvidence = BrokerCleanupEvidence;
393
393
  type CleanupIdentity = {
394
394
  dev: bigint;
395
395
  ino: bigint;
396
+ nlink?: bigint;
396
397
  size: number;
397
398
  mtimeNs: bigint;
398
399
  sha256: string;
@@ -402,6 +403,7 @@ function serializeCleanupIdentity(identity: CleanupIdentity): BrokerCleanupIdent
402
403
  return {
403
404
  dev: identity.dev.toString(),
404
405
  ino: identity.ino.toString(),
406
+ ...(identity.nlink !== undefined ? { nlink: identity.nlink.toString() } : {}),
405
407
  size: identity.size,
406
408
  mtimeNs: identity.mtimeNs.toString(),
407
409
  sha256: identity.sha256,
@@ -1039,7 +1041,7 @@ async function readLifecycleFailureArtifact(
1039
1041
  artifact: LifecycleFailureArtifact;
1040
1042
  bytes: Buffer;
1041
1043
  digest: string;
1042
- identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string };
1044
+ identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint };
1043
1045
  }
1044
1046
  | undefined
1045
1047
  > {
@@ -1047,7 +1049,7 @@ async function readLifecycleFailureArtifact(
1047
1049
  try {
1048
1050
  handle = await fs.open(file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
1049
1051
  const stat = await handle.stat({ bigint: true });
1050
- if (!stat.isFile() || stat.size > 4096n) return undefined;
1052
+ if (!stat.isFile() || stat.nlink !== 1n || stat.size > 4096n) return undefined;
1051
1053
  const bytes = Buffer.alloc(Number(stat.size) + 1);
1052
1054
  const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
1053
1055
  if (bytesRead > 4096) return undefined;
@@ -1066,6 +1068,7 @@ async function readLifecycleFailureArtifact(
1066
1068
  identity: {
1067
1069
  dev: stat.dev,
1068
1070
  ino: stat.ino,
1071
+ nlink: stat.nlink,
1069
1072
  size: stat.size,
1070
1073
  mtimeNs: stat.mtimeNs,
1071
1074
  sha256: createHash("sha256").update(raw).digest("hex"),
@@ -1080,16 +1083,21 @@ async function readLifecycleFailureArtifact(
1080
1083
 
1081
1084
  function exactUnlinkLifecycleFile(
1082
1085
  file: string,
1083
- identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string },
1086
+ identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint },
1084
1087
  plannedPath: string,
1088
+ parentIdentity?: { dev: bigint; ino: bigint },
1085
1089
  ): NativeExactUnlinkResult {
1086
- return native.exactUnlink(file, { ...identity, quarantineName: path.basename(plannedPath) });
1090
+ return native.exactUnlink(file, {
1091
+ ...identity,
1092
+ quarantineName: path.basename(plannedPath),
1093
+ ...(parentIdentity ? { parentDev: parentIdentity.dev, parentIno: parentIdentity.ino } : {}),
1094
+ });
1087
1095
  }
1088
1096
 
1089
1097
  type LifecycleCleanupFile = NonNullable<BrokerCleanupEvidence["lifecycleFiles"]>[number];
1090
1098
 
1091
1099
  function sameLifecycleCleanupIdentity(
1092
- left: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string },
1100
+ left: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint },
1093
1101
  right: BrokerCleanupIdentity,
1094
1102
  ): boolean {
1095
1103
  return (
@@ -1097,17 +1105,30 @@ function sameLifecycleCleanupIdentity(
1097
1105
  left.ino.toString() === right.ino &&
1098
1106
  left.size === BigInt(right.size) &&
1099
1107
  left.mtimeNs.toString() === right.mtimeNs &&
1100
- left.sha256 === right.sha256
1108
+ left.sha256 === right.sha256 &&
1109
+ (right.nlink === undefined || left.nlink.toString() === right.nlink)
1101
1110
  );
1102
1111
  }
1103
1112
 
1113
+ function lifecycleParentIdentity(directory: string): { dev: string; ino: string } | undefined {
1114
+ try {
1115
+ const stat = fsSync.lstatSync(directory, { bigint: true });
1116
+ if (!stat.isDirectory()) return undefined;
1117
+ return { dev: stat.dev.toString(), ino: stat.ino.toString() };
1118
+ } catch {
1119
+ return undefined;
1120
+ }
1121
+ }
1122
+
1104
1123
  function lifecycleCleanupPlan(
1105
1124
  root: string,
1106
1125
  id: string,
1107
1126
  expected: EffectMarker,
1108
- evidence: { identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string } },
1127
+ evidence: { identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint } },
1109
1128
  ): CleanupEvidence {
1110
1129
  const directory = path.join(root, "sdk");
1130
+ const parentIdentity = lifecycleParentIdentity(directory);
1131
+ if (!parentIdentity) throw new Error("Lifecycle cleanup parent identity is unavailable.");
1111
1132
  const candidates = [
1112
1133
  lifecycleFailurePath(root, id, expected.effectMarker),
1113
1134
  path.join(directory, `${id}.json`),
@@ -1162,7 +1183,13 @@ function lifecycleCleanupPlan(
1162
1183
  },
1163
1184
  ];
1164
1185
  });
1165
- return { phase: "lifecycle", sessionId: id, metadataRoot: root, lifecycleFiles: files };
1186
+ return {
1187
+ phase: "lifecycle",
1188
+ sessionId: id,
1189
+ metadataRoot: root,
1190
+ lifecycleParentIdentity: parentIdentity,
1191
+ lifecycleFiles: files,
1192
+ };
1166
1193
  }
1167
1194
 
1168
1195
  function escapeRegExp(value: string): string {
@@ -1196,14 +1223,21 @@ function isCleanupIdentity(value: unknown): value is BrokerCleanupIdentity {
1196
1223
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1197
1224
  const identity = value as Record<string, unknown>;
1198
1225
  return (
1199
- Object.keys(identity).length === 5 &&
1226
+ (Object.keys(identity).length === 5 || Object.keys(identity).length === 6) &&
1200
1227
  Object.keys(identity).every(
1201
- key => key === "dev" || key === "ino" || key === "size" || key === "mtimeNs" || key === "sha256",
1228
+ key =>
1229
+ key === "dev" ||
1230
+ key === "ino" ||
1231
+ key === "nlink" ||
1232
+ key === "size" ||
1233
+ key === "mtimeNs" ||
1234
+ key === "sha256",
1202
1235
  ) &&
1203
1236
  typeof identity.dev === "string" &&
1204
1237
  /^\d+$/.test(identity.dev) &&
1205
1238
  typeof identity.ino === "string" &&
1206
1239
  /^\d+$/.test(identity.ino) &&
1240
+ (identity.nlink === undefined || (typeof identity.nlink === "string" && /^\d+$/.test(identity.nlink))) &&
1207
1241
  typeof identity.size === "number" &&
1208
1242
  Number.isSafeInteger(identity.size) &&
1209
1243
  identity.size >= 0 &&
@@ -1226,6 +1260,9 @@ function isLifecycleCleanupFile(value: unknown): value is LifecycleCleanupFile {
1226
1260
  file.plannedPath.length > 0 &&
1227
1261
  isCleanupIdentity(file.identity) &&
1228
1262
  typeof file.attempt === "number" &&
1263
+ (file.completed === true ||
1264
+ (typeof (file.identity as BrokerCleanupIdentity).nlink === "string" &&
1265
+ (file.identity as BrokerCleanupIdentity).nlink === "1")) &&
1229
1266
  Number.isSafeInteger(file.attempt) &&
1230
1267
  file.attempt > 0 &&
1231
1268
  (file.detachedPath === undefined || (typeof file.detachedPath === "string" && file.detachedPath.length > 0)) &&
@@ -1236,7 +1273,15 @@ function isLifecycleCleanupFile(value: unknown): value is LifecycleCleanupFile {
1236
1273
  function isLifecycleCleanupEvidence(cleanup: CleanupEvidence): boolean {
1237
1274
  if (typeof cleanup !== "object" || cleanup === null || Array.isArray(cleanup)) return false;
1238
1275
  const record = cleanup as Record<string, unknown>;
1239
- const allowed = new Set(["phase", "sessionId", "metadataRoot", "lifecycleDeleteMetadata", "lifecycleFiles"]);
1276
+ const allowed = new Set([
1277
+ "phase",
1278
+ "sessionId",
1279
+ "metadataRoot",
1280
+ "lifecycleDeleteMetadata",
1281
+ "lifecycleParentIdentity",
1282
+ "lifecycleFiles",
1283
+ ]);
1284
+ const parentIdentity = record.lifecycleParentIdentity as Record<string, unknown> | undefined;
1240
1285
  return (
1241
1286
  Object.keys(record).every(key => allowed.has(key)) &&
1242
1287
  record.phase === "lifecycle" &&
@@ -1244,6 +1289,11 @@ function isLifecycleCleanupEvidence(cleanup: CleanupEvidence): boolean {
1244
1289
  isCanonicalSessionId(record.sessionId) &&
1245
1290
  typeof record.metadataRoot === "string" &&
1246
1291
  record.metadataRoot.length > 0 &&
1292
+ !!parentIdentity &&
1293
+ typeof parentIdentity.dev === "string" &&
1294
+ /^\d+$/.test(parentIdentity.dev) &&
1295
+ typeof parentIdentity.ino === "string" &&
1296
+ /^\d+$/.test(parentIdentity.ino) &&
1247
1297
  Array.isArray(record.lifecycleFiles) &&
1248
1298
  record.lifecycleFiles.length > 0 &&
1249
1299
  record.lifecycleFiles.length <= 4 &&
@@ -1487,6 +1537,8 @@ function legacyMetadataCleanupPlan(cleanup: CleanupEvidence): CleanupEvidence |
1487
1537
  return undefined;
1488
1538
  const root = path.resolve(cleanup.metadataRoot);
1489
1539
  const directory = path.join(root, "sdk");
1540
+ const parentIdentity = lifecycleParentIdentity(directory);
1541
+ if (!parentIdentity) return undefined;
1490
1542
  const markerPath = lifecycleMarkerPath(root, cleanup.sessionId);
1491
1543
  const readyPath = lifecycleReadyPath(root, cleanup.sessionId);
1492
1544
  const metadataPath = path.resolve(cleanup.metadataPath);
@@ -1501,7 +1553,7 @@ function legacyMetadataCleanupPlan(cleanup: CleanupEvidence): CleanupEvidence |
1501
1553
  (!Number.isSafeInteger(cleanup.metadataAttempt) || cleanup.metadataAttempt < 1))
1502
1554
  )
1503
1555
  return undefined;
1504
- const persistedIdentity = cleanupIdentity(cleanup.metadataIdentity);
1556
+ const persistedIdentity = cleanupIdentity(cleanup.metadataIdentity, false, false);
1505
1557
  if (!persistedIdentity) return undefined;
1506
1558
 
1507
1559
  const captureExactRegular = (
@@ -1569,6 +1621,7 @@ function legacyMetadataCleanupPlan(cleanup: CleanupEvidence): CleanupEvidence |
1569
1621
  sessionId: cleanup.sessionId,
1570
1622
  metadataRoot: root,
1571
1623
  lifecycleDeleteMetadata: true,
1624
+ lifecycleParentIdentity: parentIdentity,
1572
1625
  lifecycleFiles: [
1573
1626
  {
1574
1627
  path: metadataPath,
@@ -1603,16 +1656,28 @@ function lifecycleDeleteMetadataCleanupPlan(
1603
1656
  id: string,
1604
1657
  files: ReadonlyArray<{ metadataPath: string; metadata: LifecycleFileCapture }>,
1605
1658
  ): CleanupEvidence {
1659
+ if (files.length === 0)
1660
+ return {
1661
+ phase: "lifecycle",
1662
+ sessionId: id,
1663
+ metadataRoot,
1664
+ lifecycleDeleteMetadata: true,
1665
+ lifecycleFiles: [],
1666
+ };
1667
+ const parentIdentity = lifecycleParentIdentity(path.join(metadataRoot, "sdk"));
1668
+ if (!parentIdentity) throw new Error("Lifecycle cleanup parent identity is unavailable.");
1606
1669
  return {
1607
1670
  phase: "lifecycle",
1608
1671
  sessionId: id,
1609
1672
  metadataRoot,
1610
1673
  lifecycleDeleteMetadata: true,
1674
+ lifecycleParentIdentity: parentIdentity,
1611
1675
  lifecycleFiles: files.map(({ metadataPath, metadata }) => ({
1612
1676
  path: metadataPath,
1613
1677
  identity: serializeCleanupIdentity({
1614
1678
  dev: metadata.identity.dev,
1615
1679
  ino: metadata.identity.ino,
1680
+ nlink: metadata.identity.nlink,
1616
1681
  size: Number(metadata.identity.size),
1617
1682
  mtimeNs: metadata.identity.mtimeNs,
1618
1683
  sha256: metadata.identity.sha256,
@@ -1795,6 +1860,8 @@ async function reconcileLifecycleCleanup(
1795
1860
  const currentFile = activeCleanup.lifecycleFiles![index];
1796
1861
  const result = native.exactUnlink(activePath, {
1797
1862
  ...captured.identity,
1863
+ parentDev: BigInt(activeCleanup.lifecycleParentIdentity!.dev),
1864
+ parentIno: BigInt(activeCleanup.lifecycleParentIdentity!.ino),
1798
1865
  quarantineName: path.basename(currentFile.plannedPath),
1799
1866
  });
1800
1867
  if (!result.ok) {
@@ -1900,7 +1967,7 @@ async function hasOwnedReadinessEvidence(
1900
1967
 
1901
1968
  type LifecycleFileCapture = {
1902
1969
  bytes: Buffer;
1903
- identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string };
1970
+ identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint };
1904
1971
  digest: string;
1905
1972
  };
1906
1973
 
@@ -1919,6 +1986,7 @@ function captureLifecycleFile(file: string, requireRegular = false, bounded = fa
1919
1986
  const stat = fsSync.fstatSync(descriptor, { bigint: true });
1920
1987
  if (
1921
1988
  !stat.isFile() ||
1989
+ stat.nlink !== 1n ||
1922
1990
  (bounded &&
1923
1991
  (stat.size === 0n ||
1924
1992
  stat.size > BigInt(MAX_LIFECYCLE_METADATA_BYTES) ||
@@ -1945,6 +2013,7 @@ function captureLifecycleFile(file: string, requireRegular = false, bounded = fa
1945
2013
  dev: stat.dev,
1946
2014
  ino: stat.ino,
1947
2015
  size: stat.size,
2016
+ nlink: stat.nlink,
1948
2017
  mtimeNs: stat.mtimeNs,
1949
2018
  sha256: createHash("sha256").update(bytes).digest("hex"),
1950
2019
  },
@@ -1982,7 +2051,8 @@ async function removeOwnedLifecycleArtifacts(root: string, id: string, expected:
1982
2051
  }
1983
2052
  });
1984
2053
  const endpoint = endpointSource ? captureLifecycleFile(endpointSource) : undefined;
1985
- if (endpoint && endpointSource) {
2054
+ const endpointParent = endpointSource ? lifecycleParentIdentity(path.dirname(endpointSource)) : undefined;
2055
+ if (endpoint && endpointSource && endpointParent) {
1986
2056
  let parsed: { pid?: unknown };
1987
2057
  try {
1988
2058
  parsed = parseLifecycleJson(endpoint.bytes) as { pid?: unknown };
@@ -1999,6 +2069,7 @@ async function removeOwnedLifecycleArtifacts(root: string, id: string, expected:
1999
2069
  : endpointSource === plannedEndpointPath
2000
2070
  ? retryEndpointPath
2001
2071
  : finalEndpointPath,
2072
+ { dev: BigInt(endpointParent.dev), ino: BigInt(endpointParent.ino) },
2002
2073
  );
2003
2074
  if (!endpointRemoval.ok) return false;
2004
2075
  }
@@ -2444,15 +2515,18 @@ type ValidatedDelete = {
2444
2515
  storage: FileSessionStorage;
2445
2516
  target: VerifiedSessionDeleteTarget;
2446
2517
  metadataRoot: string;
2518
+ transcriptParentIdentity: { dev: string; ino: string };
2447
2519
  };
2448
2520
  function cleanupIdentity(
2449
2521
  identity: BrokerCleanupEvidence["transcriptIdentity"],
2450
2522
  allowEmptySha256 = false,
2451
- ): SessionStorageFileIdentity | undefined {
2523
+ requireNlink = true,
2524
+ ): CleanupIdentity | undefined {
2452
2525
  if (
2453
2526
  !identity ||
2454
2527
  !/^[0-9]+$/.test(identity.dev) ||
2455
2528
  !/^[0-9]+$/.test(identity.ino) ||
2529
+ (requireNlink && (typeof identity.nlink !== "string" || !/^[0-9]+$/.test(identity.nlink))) ||
2456
2530
  !Number.isSafeInteger(identity.size) ||
2457
2531
  identity.size < 0 ||
2458
2532
  !/^[0-9]+$/.test(identity.mtimeNs) ||
@@ -2463,6 +2537,7 @@ function cleanupIdentity(
2463
2537
  return {
2464
2538
  dev: BigInt(identity.dev),
2465
2539
  ino: BigInt(identity.ino),
2540
+ ...(typeof identity.nlink === "string" ? { nlink: BigInt(identity.nlink) } : {}),
2466
2541
  size: identity.size,
2467
2542
  mtimeNs: BigInt(identity.mtimeNs),
2468
2543
  sha256: identity.sha256,
@@ -2470,7 +2545,12 @@ function cleanupIdentity(
2470
2545
  }
2471
2546
 
2472
2547
  function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerResponse {
2473
- const transcriptIdentity = cleanupIdentity(cleanup.transcriptIdentity);
2548
+ const parsedTranscriptIdentity = cleanupIdentity(cleanup.transcriptIdentity);
2549
+ const transcriptIdentity: SessionStorageFileIdentity | undefined =
2550
+ parsedTranscriptIdentity?.nlink !== undefined
2551
+ ? { ...parsedTranscriptIdentity, nlink: parsedTranscriptIdentity.nlink }
2552
+ : undefined;
2553
+ const transcriptParentIdentity = cleanup.transcriptParentIdentity;
2474
2554
  if (
2475
2555
  (cleanup.phase !== "artifacts" && cleanup.phase !== "transcript") ||
2476
2556
  !cleanup.sessionId ||
@@ -2479,20 +2559,74 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
2479
2559
  !cleanup.transcriptPath ||
2480
2560
  !cleanup.cwd ||
2481
2561
  !cleanup.metadataRoot ||
2482
- !transcriptIdentity
2562
+ !transcriptIdentity ||
2563
+ !transcriptParentIdentity ||
2564
+ !/^[0-9]+$/.test(transcriptParentIdentity.dev) ||
2565
+ !/^[0-9]+$/.test(transcriptParentIdentity.ino)
2483
2566
  ) {
2484
2567
  return fail("terminal_uncertain", "Cleanup replay lacks a complete ledger-bound deletion target.");
2485
2568
  }
2486
- const artifactsIdentity = cleanupIdentity(cleanup.artifactsIdentity, true);
2487
- const artifactTreeIdentity =
2488
- cleanup.artifactsRemoved === true || !cleanup.artifactTree
2489
- ? undefined
2490
- : cleanupIdentity(cleanup.artifactTree.identity, true);
2491
- if (cleanup.artifactsRemoved !== true && cleanup.artifactTree && !artifactTreeIdentity)
2569
+ const artifactsIdentity = cleanupIdentity(cleanup.artifactsIdentity, true, false);
2570
+ const artifactTreeIdentity = cleanup.artifactTree
2571
+ ? cleanupIdentity(cleanup.artifactTree.identity, true, false)
2572
+ : undefined;
2573
+ if (cleanup.artifactsAbsentAtAuthorization !== undefined && cleanup.artifactsAbsentAtAuthorization !== true)
2574
+ return fail("terminal_uncertain", "Artifact absence authority is malformed.");
2575
+ if (
2576
+ cleanup.artifactsAbsentAtAuthorization === true &&
2577
+ (cleanup.artifactsRemoved === true || artifactsIdentity !== undefined || cleanup.artifactTree !== undefined)
2578
+ )
2579
+ return fail("terminal_uncertain", "Artifact absence authority contradicts retained or completed artifacts.");
2580
+ if (cleanup.artifactTree && !artifactTreeIdentity)
2492
2581
  return fail("terminal_uncertain", "Artifact tree cleanup lacks its ledger-bound identity.");
2493
-
2494
- if (cleanup.detachedArtifactsPath && !artifactsIdentity)
2495
- return fail("terminal_uncertain", "Detached artifact cleanup lacks its ledger-bound identity.");
2582
+ if (cleanup.artifactsRemoved !== true && (artifactsIdentity !== undefined) !== (artifactTreeIdentity !== undefined))
2583
+ return fail("terminal_uncertain", "Artifact cleanup receipt lacks its immutable tree snapshot.");
2584
+ if (cleanup.artifactsRemoved === true && (artifactsIdentity !== undefined) !== (artifactTreeIdentity !== undefined))
2585
+ return fail("terminal_uncertain", "Artifacts-removed cleanup receipt dropped its immutable tree authority.");
2586
+ if (
2587
+ artifactsIdentity &&
2588
+ artifactTreeIdentity &&
2589
+ (artifactsIdentity.dev !== artifactTreeIdentity.dev || artifactsIdentity.ino !== artifactTreeIdentity.ino)
2590
+ )
2591
+ return fail("terminal_uncertain", "Artifact cleanup tree does not match its ledger-bound root identity.");
2592
+ if (cleanup.phase === "artifacts" && cleanup.artifactsRemoved === true)
2593
+ return fail("terminal_uncertain", "Artifacts-phase cleanup receipt falsely claims artifact completion.");
2594
+ if (
2595
+ cleanup.artifactsRemoved === true &&
2596
+ cleanup.artifactTree &&
2597
+ (cleanup.artifactTree.completed !== true || cleanup.artifactTree.detachedPath !== undefined)
2598
+ )
2599
+ return fail("terminal_uncertain", "Artifacts-removed cleanup receipt retains unfinished nested authority.");
2600
+ if (cleanup.phase === "transcript" && cleanup.artifactsRemoved !== true)
2601
+ return fail("terminal_uncertain", "Transcript cleanup lacks durable artifact completion proof.");
2602
+
2603
+ const retainedArtifactSidePaths = [
2604
+ cleanup.retainedArtifactsSuccessorPath,
2605
+ cleanup.retainedArtifactsPlaceholderPath,
2606
+ cleanup.retainedArtifactsUnknownPath,
2607
+ ];
2608
+ const retainedArtifactSideAuthority = cleanup.retainedArtifactsSideAuthority;
2609
+ const cleanupReceiptVersion = cleanup.cleanupReceiptVersion;
2610
+ const hasRetainedArtifactSidePath = retainedArtifactSidePaths.some(
2611
+ candidate => typeof candidate === "string" && candidate.length > 0,
2612
+ );
2613
+ if ((cleanup.phase === "artifacts" || cleanup.phase === "transcript") && cleanupReceiptVersion !== 1)
2614
+ return fail("terminal_uncertain", "Cleanup replay lacks supported versioned authority.");
2615
+ if (
2616
+ cleanup.artifactsRemoved === true &&
2617
+ (cleanup.detachedArtifactsPath !== undefined ||
2618
+ retainedArtifactSidePaths.some(Boolean) ||
2619
+ retainedArtifactSideAuthority === "retained")
2620
+ )
2621
+ return fail("terminal_uncertain", "Artifacts-removed cleanup receipt retains contradictory artifact authority.");
2622
+ if (
2623
+ cleanup.artifactsRemoved !== true &&
2624
+ ((retainedArtifactSideAuthority !== "none" && retainedArtifactSideAuthority !== "retained") ||
2625
+ (retainedArtifactSideAuthority === "retained") !== hasRetainedArtifactSidePath)
2626
+ )
2627
+ return fail("terminal_uncertain", "Retained artifact side authority receipt is incomplete or corrupt.");
2628
+ if ((cleanup.detachedArtifactsPath || retainedArtifactSidePaths.some(Boolean)) && !artifactsIdentity)
2629
+ return fail("terminal_uncertain", "Retained artifact cleanup lacks its ledger-bound identity.");
2496
2630
  const plannedArtifactsPath = cleanup.plannedArtifactsPath;
2497
2631
  const plannedTranscriptPath = cleanup.plannedTranscriptPath;
2498
2632
  if (
@@ -2502,22 +2636,17 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
2502
2636
  (plannedTranscriptPath &&
2503
2637
  (path.dirname(plannedTranscriptPath) !== path.dirname(cleanup.transcriptPath) ||
2504
2638
  !path.basename(plannedTranscriptPath).startsWith(".gjc-delete-"))) ||
2505
- (cleanup.artifactsRemoved !== true &&
2506
- cleanup.artifactTree &&
2639
+ (cleanup.artifactTree &&
2507
2640
  (path.dirname(cleanup.artifactTree.plannedPath) !== path.dirname(cleanup.transcriptPath) ||
2508
2641
  !path.basename(cleanup.artifactTree.plannedPath).startsWith(".gjc-delete-") ||
2509
2642
  (cleanup.artifactTree.detachedPath !== undefined &&
2510
- path.dirname(cleanup.artifactTree.detachedPath) !== path.dirname(cleanup.transcriptPath))))
2511
- )
2512
- return fail("terminal_uncertain", "Cleanup replay has invalid preauthorized quarantine paths.");
2513
- if (
2514
- cleanup.phase === "transcript" &&
2515
- cleanup.artifactsRemoved === true &&
2516
- ![cleanup.transcriptPath, cleanup.detachedTranscriptPath, plannedTranscriptPath].some(
2517
- candidate => typeof candidate === "string" && fsSync.existsSync(candidate),
2643
+ path.dirname(cleanup.artifactTree.detachedPath) !== path.dirname(cleanup.transcriptPath)))) ||
2644
+ retainedArtifactSidePaths.some(
2645
+ candidate =>
2646
+ typeof candidate === "string" && path.dirname(candidate) !== path.dirname(cleanup.transcriptPath!),
2518
2647
  )
2519
2648
  )
2520
- return { ok: true, result: { sessionId: cleanup.sessionId } };
2649
+ return fail("terminal_uncertain", "Cleanup replay has invalid preauthorized quarantine paths.");
2521
2650
  const artifactRemovingPath =
2522
2651
  cleanup.artifactsRemoved === true
2523
2652
  ? undefined
@@ -2536,6 +2665,8 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
2536
2665
  fsSync.existsSync(plannedArtifactsPath)
2537
2666
  ? plannedArtifactsPath
2538
2667
  : undefined;
2668
+ const replayPlannedArtifactsPath =
2669
+ plannedArtifactsPath !== recoveredDetachedArtifactsPath ? plannedArtifactsPath : undefined;
2539
2670
  const recoveredDetachedTranscriptPath =
2540
2671
  cleanup.detachedTranscriptPath && fsSync.existsSync(cleanup.detachedTranscriptPath)
2541
2672
  ? cleanup.detachedTranscriptPath
@@ -2552,29 +2683,76 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
2552
2683
  sessionId: cleanup.sessionId,
2553
2684
  cwd: cleanup.cwd,
2554
2685
  transcriptIdentity,
2686
+ transcriptParentIdentity: {
2687
+ dev: BigInt(cleanup.transcriptParentIdentity!.dev),
2688
+ ino: BigInt(cleanup.transcriptParentIdentity!.ino),
2689
+ },
2555
2690
  ...(cleanup.artifactsRemoved === true ? { artifactsRemoved: true } : {}),
2691
+ ...(cleanup.artifactsAbsentAtAuthorization === true ? { artifactsAbsentAtAuthorization: true as const } : {}),
2556
2692
  ...(cleanup.artifactsRemoved !== true && artifactsIdentity
2557
2693
  ? { expectedArtifactsIdentity: artifactsIdentity }
2558
2694
  : {}),
2559
2695
  ...(cleanup.artifactsRemoved !== true && recoveredDetachedArtifactsPath
2560
2696
  ? { detachedArtifactsPath: recoveredDetachedArtifactsPath }
2561
2697
  : {}),
2698
+ ...(cleanup.artifactsRemoved !== true && cleanup.retainedArtifactsSuccessorPath
2699
+ ? { retainedArtifactsSuccessorPath: cleanup.retainedArtifactsSuccessorPath }
2700
+ : {}),
2701
+ ...(cleanup.artifactsRemoved !== true && cleanup.retainedArtifactsPlaceholderPath
2702
+ ? { retainedArtifactsPlaceholderPath: cleanup.retainedArtifactsPlaceholderPath }
2703
+ : {}),
2704
+ ...(cleanup.artifactsRemoved !== true && cleanup.retainedArtifactsUnknownPath
2705
+ ? { retainedArtifactsUnknownPath: cleanup.retainedArtifactsUnknownPath }
2706
+ : {}),
2562
2707
  ...(recoveredDetachedTranscriptPath ? { detachedTranscriptPath: recoveredDetachedTranscriptPath } : {}),
2563
- ...(plannedArtifactsPath ? { plannedArtifactsPath } : {}),
2708
+ ...(cleanup.retainedTranscriptSuccessorPath
2709
+ ? { retainedTranscriptSuccessorPath: cleanup.retainedTranscriptSuccessorPath }
2710
+ : {}),
2711
+ ...(cleanup.retainedTranscriptPlaceholderPath
2712
+ ? { retainedTranscriptPlaceholderPath: cleanup.retainedTranscriptPlaceholderPath }
2713
+ : {}),
2714
+ ...(cleanup.retainedTranscriptUnknownPath
2715
+ ? { retainedTranscriptUnknownPath: cleanup.retainedTranscriptUnknownPath }
2716
+ : {}),
2717
+ ...(replayPlannedArtifactsPath ? { plannedArtifactsPath: replayPlannedArtifactsPath } : {}),
2564
2718
  ...(plannedTranscriptPath ? { plannedTranscriptPath } : {}),
2565
2719
  ...(cleanup.artifactsRemoved !== true && cleanup.artifactTree && artifactTreeIdentity
2566
2720
  ? {
2567
2721
  expectedArtifactsIdentity: artifactTreeIdentity,
2568
2722
  expectedArtifactsTree: cleanup.artifactTree.snapshot,
2569
2723
  detachedArtifactsPath: cleanup.artifactTree.detachedPath ?? recoveredDetachedArtifactsPath,
2570
- plannedArtifactsPath: cleanup.artifactTree.plannedPath,
2724
+ ...(replayPlannedArtifactsPath ? { plannedArtifactsPath: replayPlannedArtifactsPath } : {}),
2571
2725
  }
2572
2726
  : {}),
2573
2727
  },
2574
2728
  metadataRoot: cleanup.metadataRoot,
2729
+ transcriptParentIdentity: cleanup.transcriptParentIdentity!,
2575
2730
  };
2576
2731
  }
2577
2732
 
2733
+ function canonicalExistingPath(pathname: string): string {
2734
+ try {
2735
+ return fsSync.realpathSync.native(pathname);
2736
+ } catch {
2737
+ return path.resolve(pathname);
2738
+ }
2739
+ }
2740
+
2741
+ export function canonicalDeleteLocatorPath(pathname: string): string {
2742
+ let current = path.resolve(pathname);
2743
+ const suffix: string[] = [];
2744
+ for (;;) {
2745
+ try {
2746
+ return path.join(fsSync.realpathSync.native(current), ...suffix.reverse());
2747
+ } catch {
2748
+ const parent = path.dirname(current);
2749
+ if (parent === current) return path.resolve(pathname);
2750
+ suffix.push(path.basename(current));
2751
+ current = parent;
2752
+ }
2753
+ }
2754
+ }
2755
+
2578
2756
  async function validateDeletePath(
2579
2757
  broker: Broker,
2580
2758
  input: Input,
@@ -2582,25 +2760,46 @@ async function validateDeletePath(
2582
2760
  record: { locator: { repo: string; stateRoot: string } } | undefined,
2583
2761
  cleanup?: CleanupEvidence,
2584
2762
  ): Promise<ValidatedDelete | BrokerResponse> {
2585
- if (cleanup) return replayDeleteTarget(cleanup);
2586
2763
  const sessionPath = text(input.sessionPath);
2587
- const cwd = lifecycleCwd(input);
2588
- if (!sessionPath || !cwd)
2764
+ const lexicalCwd = lifecycleCwd(input);
2765
+ if (!sessionPath || !lexicalCwd)
2589
2766
  return fail("invalid_input", "session.delete requires sessionPath and its configured cwd.");
2590
- const requestedRoot = stateRoot(input, cwd);
2591
- if (!requestedRoot || !hasDefaultStateRoot(cwd, requestedRoot))
2767
+ const requestedRoot = stateRoot(input, lexicalCwd);
2768
+ if (!requestedRoot || !hasDefaultStateRoot(lexicalCwd, requestedRoot))
2592
2769
  return fail("invalid_input", "stateRoot must be the default .gjc/state for cwd.");
2770
+ const cwd = canonicalExistingPath(lexicalCwd);
2771
+ const canonicalRequestedRoot = canonicalExistingPath(requestedRoot);
2593
2772
  if (
2594
2773
  record &&
2595
- (path.resolve(record.locator.repo) !== cwd || path.resolve(record.locator.stateRoot) !== requestedRoot)
2774
+ (canonicalExistingPath(record.locator.repo) !== cwd ||
2775
+ canonicalExistingPath(record.locator.stateRoot) !== canonicalRequestedRoot)
2596
2776
  )
2597
2777
  return fail("invalid_input", "session.delete locator does not match the indexed session.");
2598
-
2778
+ const candidatePath = canonicalDeleteLocatorPath(sessionPath);
2779
+ let transcriptParentStat: fsSync.BigIntStats;
2780
+ try {
2781
+ transcriptParentStat = fsSync.lstatSync(path.dirname(candidatePath), { bigint: true });
2782
+ if (!transcriptParentStat.isDirectory())
2783
+ return fail("invalid_input", "session.delete transcript parent is not a directory.");
2784
+ } catch {
2785
+ return fail("invalid_input", "session.delete transcript parent cannot be authorized.");
2786
+ }
2787
+ if (cleanup) {
2788
+ const replay = replayDeleteTarget(cleanup);
2789
+ if ("ok" in replay) return replay;
2790
+ if (
2791
+ replay.target.sessionId !== id ||
2792
+ canonicalDeleteLocatorPath(replay.target.transcriptPath) !== candidatePath ||
2793
+ canonicalExistingPath(replay.target.cwd) !== cwd ||
2794
+ canonicalExistingPath(replay.metadataRoot) !== canonicalRequestedRoot
2795
+ )
2796
+ return fail("invalid_input", "Cleanup receipt does not match the requested saved-session locator.");
2797
+ return replay;
2798
+ }
2599
2799
  const inventory = await managedCandidates(broker, cwd, "Saved");
2600
2800
  if ("ok" in inventory) return inventory;
2601
- const candidatePath = path.resolve(sessionPath);
2602
2801
  const matches = inventory.candidates.filter(
2603
- candidate => candidate.path === candidatePath && candidate.sessionId === id,
2802
+ candidate => canonicalExistingPath(candidate.path) === candidatePath && candidate.sessionId === id,
2604
2803
  );
2605
2804
  if (matches.length !== 1)
2606
2805
  return fail("invalid_input", "session.delete path is not an owned managed session for the configured cwd.");
@@ -2619,27 +2818,45 @@ async function validateDeletePath(
2619
2818
  if (
2620
2819
  snapshot.stat.dev !== match.identity.dev ||
2621
2820
  snapshot.stat.ino !== match.identity.ino ||
2821
+ snapshot.stat.nlink !== 1n ||
2622
2822
  snapshot.stat.size !== match.identity.size ||
2623
2823
  snapshot.stat.mtimeNs !== match.identity.mtimeNs ||
2624
2824
  digest !== match.identity.sha256
2625
2825
  )
2626
2826
  return fail("invalid_input", "session.delete session changed after managed ownership was verified.");
2827
+ try {
2828
+ const currentParent = fsSync.lstatSync(path.dirname(candidatePath), { bigint: true });
2829
+ if (
2830
+ !currentParent.isDirectory() ||
2831
+ currentParent.dev !== transcriptParentStat.dev ||
2832
+ currentParent.ino !== transcriptParentStat.ino
2833
+ )
2834
+ return fail("invalid_input", "session.delete transcript parent changed during authorization.");
2835
+ } catch {
2836
+ return fail("invalid_input", "session.delete transcript parent changed during authorization.");
2837
+ }
2627
2838
  return {
2628
2839
  storage,
2629
2840
  target: {
2630
- sessionsRoot: inventory.scope.sessionsRoot,
2841
+ sessionsRoot: canonicalExistingPath(inventory.scope.sessionsRoot),
2631
2842
  transcriptPath: candidatePath,
2632
2843
  sessionId: id,
2633
2844
  cwd,
2634
2845
  transcriptIdentity: {
2635
2846
  dev: snapshot.stat.dev,
2636
2847
  ino: snapshot.stat.ino,
2848
+ nlink: snapshot.stat.nlink,
2637
2849
  size: snapshot.stat.size,
2638
2850
  mtimeNs: snapshot.stat.mtimeNs,
2639
2851
  sha256: digest,
2640
2852
  },
2853
+ transcriptParentIdentity: { dev: transcriptParentStat.dev, ino: transcriptParentStat.ino },
2854
+ },
2855
+ metadataRoot: canonicalRequestedRoot,
2856
+ transcriptParentIdentity: {
2857
+ dev: transcriptParentStat.dev.toString(),
2858
+ ino: transcriptParentStat.ino.toString(),
2641
2859
  },
2642
- metadataRoot: requestedRoot,
2643
2860
  };
2644
2861
  }
2645
2862
  type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
@@ -2762,6 +2979,12 @@ async function executeLifecycleResponse(
2762
2979
  const requestedSessionId = cleanup && operation === "session.delete" ? cleanup.sessionId : sessionId(input);
2763
2980
  if (requestedSessionId !== undefined && !isCanonicalSessionId(requestedSessionId))
2764
2981
  return fail("invalid_input", "sessionId must be a canonical safe identifier.");
2982
+ if (
2983
+ operation === "session.delete" &&
2984
+ requestedSessionId &&
2985
+ broker.ledger.hasUncertainCleanupForSession(requestedSessionId, identity)
2986
+ )
2987
+ return fail("terminal_uncertain", "Prior cleanup authority for this session is corrupt or incomplete.");
2765
2988
  const requestedSourceSessionId = text(input.sourceSessionId) ?? text(input.sourceId);
2766
2989
  if (requestedSourceSessionId !== undefined && !isCanonicalSessionId(requestedSourceSessionId))
2767
2990
  return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
@@ -3153,6 +3376,27 @@ async function executeLifecycleResponse(
3153
3376
  if (record?.terminalUncertain)
3154
3377
  return fail("terminal_uncertain", "Session ownership is uncertain and cannot be deleted safely.");
3155
3378
  if (record?.live) return fail("live_session", "Refusing to delete a live session; close it first.");
3379
+ if (cleanup === undefined) {
3380
+ const requestedTranscriptPath = text(input.sessionPath);
3381
+ const requestedCwd = lifecycleCwd(input);
3382
+ if (requestedTranscriptPath && requestedCwd) {
3383
+ const transcriptPath = canonicalExistingPath(requestedTranscriptPath);
3384
+ const foreignCleanup = broker.ledger.findCleanupPendingByDeleteTarget(
3385
+ {
3386
+ sessionId: id,
3387
+ transcriptPath,
3388
+ cwd: canonicalExistingPath(requestedCwd),
3389
+ },
3390
+ identity,
3391
+ );
3392
+ if (foreignCleanup) {
3393
+ const response = foreignCleanup.response as BrokerResponse | undefined;
3394
+ if (!response || response.ok || response.error.code !== "cleanup_pending" || !response.error.cleanup)
3395
+ return fail("terminal_uncertain", "Session cleanup authority is incomplete or corrupt.");
3396
+ return response;
3397
+ }
3398
+ }
3399
+ }
3156
3400
  const validated = await validateDeletePath(broker, input, id, record, cleanup);
3157
3401
  if ("ok" in validated) return validated;
3158
3402
  const metadataPreflight = preflightLifecycleDeleteMetadata(validated.metadataRoot, id, record, value =>
@@ -3207,36 +3451,264 @@ async function executeLifecycleResponse(
3207
3451
  };
3208
3452
  } catch (error) {
3209
3453
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
3454
+ cleanupTarget = { ...cleanupTarget, artifactsAbsentAtAuthorization: true };
3210
3455
  }
3211
3456
  }
3457
+ const pathIsAbsent = (candidate: string | undefined): boolean => {
3458
+ if (!candidate) return true;
3459
+ try {
3460
+ fsSync.lstatSync(candidate);
3461
+ return false;
3462
+ } catch (error) {
3463
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
3464
+ }
3465
+ };
3466
+ const transcriptParentIdentity = cleanup?.transcriptParentIdentity ?? validated.transcriptParentIdentity;
3467
+ const durableArtifactsPlan =
3468
+ cleanup?.artifactTree?.plannedPath ?? cleanup?.plannedArtifactsPath ?? cleanupTarget.plannedArtifactsPath;
3212
3469
  const preauthorizedCleanup: CleanupEvidence = {
3470
+ cleanupReceiptVersion: 1,
3213
3471
  phase: cleanupTarget.artifactsRemoved ? "transcript" : "artifacts",
3214
3472
  sessionId: cleanupTarget.sessionId,
3215
3473
  sessionsRoot: cleanupTarget.sessionsRoot,
3216
3474
  transcriptPath: cleanupTarget.transcriptPath,
3217
3475
  cwd: cleanupTarget.cwd,
3218
3476
  ...(cleanupTarget.artifactsRemoved ? { artifactsRemoved: true } : {}),
3477
+ ...(cleanupTarget.artifactsAbsentAtAuthorization ? { artifactsAbsentAtAuthorization: true as const } : {}),
3219
3478
  metadataRoot: validated.metadataRoot,
3220
3479
  transcriptIdentity: serializeCleanupIdentity(cleanupTarget.transcriptIdentity),
3480
+ transcriptParentIdentity,
3221
3481
  ...(cleanupTarget.expectedArtifactsIdentity
3222
3482
  ? { artifactsIdentity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity) }
3223
3483
  : {}),
3224
- ...(cleanupTarget.expectedArtifactsIdentity && cleanupTarget.plannedArtifactsPath
3484
+ ...(cleanupTarget.expectedArtifactsIdentity && durableArtifactsPlan
3225
3485
  ? {
3226
3486
  artifactTree: {
3227
3487
  identity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity),
3228
3488
  snapshot: cleanupTarget.expectedArtifactsTree!,
3229
- plannedPath: cleanupTarget.plannedArtifactsPath,
3489
+ plannedPath: durableArtifactsPlan,
3230
3490
  ...(cleanupTarget.detachedArtifactsPath
3231
3491
  ? { detachedPath: cleanupTarget.detachedArtifactsPath }
3232
3492
  : {}),
3233
3493
  },
3234
3494
  }
3235
3495
  : {}),
3496
+ ...(cleanup?.artifactTree ? { artifactTree: cleanup.artifactTree } : {}),
3497
+ ...(cleanup?.artifactsIdentity ? { artifactsIdentity: cleanup.artifactsIdentity } : {}),
3498
+ ...(cleanupTarget.retainedArtifactsSuccessorPath
3499
+ ? { retainedArtifactsSuccessorPath: cleanupTarget.retainedArtifactsSuccessorPath }
3500
+ : {}),
3501
+ ...(cleanupTarget.retainedArtifactsPlaceholderPath
3502
+ ? { retainedArtifactsPlaceholderPath: cleanupTarget.retainedArtifactsPlaceholderPath }
3503
+ : {}),
3504
+ ...(cleanupTarget.retainedArtifactsUnknownPath
3505
+ ? { retainedArtifactsUnknownPath: cleanupTarget.retainedArtifactsUnknownPath }
3506
+ : {}),
3507
+ retainedArtifactsSideAuthority: [
3508
+ cleanupTarget.retainedArtifactsSuccessorPath,
3509
+ cleanupTarget.retainedArtifactsPlaceholderPath,
3510
+ cleanupTarget.retainedArtifactsUnknownPath,
3511
+ ].some(candidate => candidate !== undefined)
3512
+ ? "retained"
3513
+ : "none",
3514
+ ...(cleanupTarget.detachedTranscriptPath
3515
+ ? { detachedTranscriptPath: cleanupTarget.detachedTranscriptPath }
3516
+ : {}),
3517
+ ...(cleanupTarget.retainedTranscriptSuccessorPath
3518
+ ? { retainedTranscriptSuccessorPath: cleanupTarget.retainedTranscriptSuccessorPath }
3519
+ : {}),
3520
+ ...(cleanupTarget.retainedTranscriptPlaceholderPath
3521
+ ? { retainedTranscriptPlaceholderPath: cleanupTarget.retainedTranscriptPlaceholderPath }
3522
+ : {}),
3523
+ ...(cleanupTarget.retainedTranscriptUnknownPath
3524
+ ? { retainedTranscriptUnknownPath: cleanupTarget.retainedTranscriptUnknownPath }
3525
+ : {}),
3236
3526
 
3237
3527
  ...(cleanupTarget.plannedArtifactsPath ? { plannedArtifactsPath: cleanupTarget.plannedArtifactsPath } : {}),
3238
3528
  ...(cleanupTarget.plannedTranscriptPath ? { plannedTranscriptPath: cleanupTarget.plannedTranscriptPath } : {}),
3239
3529
  };
3530
+ const publishChangedArtifactRoot = async (
3531
+ retainedPath: string | undefined,
3532
+ message = "Saved session cleanup is pending in artifacts: retained artifact root changed before exact removal.",
3533
+ ): Promise<BrokerResponse> => {
3534
+ const artifactPhaseCleanup: CleanupEvidence = {
3535
+ ...preauthorizedCleanup,
3536
+ phase: "artifacts",
3537
+ ...(retainedPath ? { detachedArtifactsPath: retainedPath } : {}),
3538
+ ...(retainedPath && preauthorizedCleanup.artifactTree
3539
+ ? {
3540
+ artifactTree: {
3541
+ ...preauthorizedCleanup.artifactTree,
3542
+ detachedPath: retainedPath,
3543
+ },
3544
+ }
3545
+ : {}),
3546
+ };
3547
+ const changedRoot = fail("cleanup_pending", message, artifactPhaseCleanup);
3548
+ await broker.ledger.transition(identity, "effect_started", {
3549
+ intendedSessionId: id,
3550
+ response: changedRoot,
3551
+ });
3552
+ return changedRoot;
3553
+ };
3554
+ const publishCanonicalArtifactReappearance = async (
3555
+ transcriptPhaseCleanup: CleanupEvidence,
3556
+ message = "Saved session cleanup is pending in artifacts: canonical artifact path reappeared before transcript reconciliation.",
3557
+ ): Promise<BrokerResponse> => {
3558
+ const pending = fail("cleanup_pending", message, transcriptPhaseCleanup);
3559
+ await broker.ledger.transition(identity, "effect_started", {
3560
+ intendedSessionId: id,
3561
+ response: pending,
3562
+ });
3563
+ return pending;
3564
+ };
3565
+ const publishRetainedTranscriptSideAuthority = async (): Promise<BrokerResponse> => {
3566
+ const pending = fail(
3567
+ "cleanup_pending",
3568
+ "Saved session cleanup is pending in transcript side authority.",
3569
+ preauthorizedCleanup,
3570
+ );
3571
+ await broker.ledger.transition(identity, "effect_started", {
3572
+ intendedSessionId: id,
3573
+ response: pending,
3574
+ });
3575
+ return pending;
3576
+ };
3577
+ const canonicalArtifactsPath = cleanupTarget.transcriptPath.slice(0, -6);
3578
+ const transcriptCleanupAuthorityIsAbsent = (): boolean =>
3579
+ [
3580
+ cleanupTarget.transcriptPath,
3581
+ cleanupTarget.detachedTranscriptPath,
3582
+ cleanupTarget.plannedTranscriptPath,
3583
+ cleanupTarget.plannedTranscriptPath ? `${cleanupTarget.plannedTranscriptPath}.removing` : undefined,
3584
+ cleanupTarget.retainedTranscriptSuccessorPath,
3585
+ cleanupTarget.retainedTranscriptPlaceholderPath,
3586
+ cleanupTarget.retainedTranscriptUnknownPath,
3587
+ ].every(pathIsAbsent);
3588
+ const artifactQuarantineAliasesAreAbsent = (allowDetachedRoot: boolean): boolean => {
3589
+ const authorizedPlans = [
3590
+ cleanup?.plannedArtifactsPath,
3591
+ cleanup?.artifactTree?.plannedPath,
3592
+ cleanupTarget.plannedArtifactsPath,
3593
+ ].filter((candidate): candidate is string => candidate !== undefined);
3594
+ return authorizedPlans.every(plannedPath =>
3595
+ [plannedPath, `${plannedPath}.removing`].every(
3596
+ aliasPath =>
3597
+ (allowDetachedRoot && aliasPath === cleanupTarget.detachedArtifactsPath) || pathIsAbsent(aliasPath),
3598
+ ),
3599
+ );
3600
+ };
3601
+ const retainedArtifactReplayHasNoSideAuthority = [
3602
+ cleanupTarget.retainedArtifactsSuccessorPath,
3603
+ cleanupTarget.retainedArtifactsPlaceholderPath,
3604
+ cleanupTarget.retainedArtifactsUnknownPath,
3605
+ ].every(candidate => candidate === undefined);
3606
+ if (cleanup && !retainedArtifactReplayHasNoSideAuthority) {
3607
+ const retainedPath = cleanupTarget.detachedArtifactsPath;
3608
+ return await publishChangedArtifactRoot(
3609
+ retainedPath,
3610
+ "Saved session cleanup is pending in artifacts: retained artifact side authority remains before transcript cleanup.",
3611
+ );
3612
+ }
3613
+ const retainedTranscriptSidePaths = [
3614
+ cleanupTarget.retainedTranscriptSuccessorPath,
3615
+ cleanupTarget.retainedTranscriptPlaceholderPath,
3616
+ cleanupTarget.retainedTranscriptUnknownPath,
3617
+ ];
3618
+ const transcriptParentMatchesPersistedIdentity = (): boolean => {
3619
+ const expectedParent = preauthorizedCleanup.transcriptParentIdentity;
3620
+ try {
3621
+ const stat = fsSync.lstatSync(path.dirname(cleanupTarget.transcriptPath), { bigint: true });
3622
+ return (
3623
+ stat.isDirectory() &&
3624
+ expectedParent !== undefined &&
3625
+ stat.dev.toString() === expectedParent.dev &&
3626
+ stat.ino.toString() === expectedParent.ino
3627
+ );
3628
+ } catch {
3629
+ return false;
3630
+ }
3631
+ };
3632
+ const retainedTranscriptIdentityIsAbsentFromParent = (): boolean => {
3633
+ const transcriptParent = path.dirname(cleanupTarget.transcriptPath);
3634
+ const expectedParent = preauthorizedCleanup.transcriptParentIdentity;
3635
+ const pendingDirectories = [transcriptParent];
3636
+ const snapshots: Array<{ path: string; stat: fsSync.BigIntStats }> = [];
3637
+ let entryCount = 0;
3638
+ try {
3639
+ while (pendingDirectories.length > 0) {
3640
+ const directory = pendingDirectories.pop();
3641
+ if (!directory) return false;
3642
+ const before = fsSync.lstatSync(directory, { bigint: true });
3643
+ if (!before.isDirectory()) return false;
3644
+ if (
3645
+ directory === transcriptParent &&
3646
+ (!expectedParent ||
3647
+ before.dev.toString() !== expectedParent.dev ||
3648
+ before.ino.toString() !== expectedParent.ino)
3649
+ )
3650
+ return false;
3651
+ snapshots.push({ path: directory, stat: before });
3652
+ const entries = fsSync.readdirSync(directory);
3653
+ entryCount += entries.length;
3654
+ if (entryCount > 10_000) return false;
3655
+ for (const entry of entries) {
3656
+ const pathname = path.join(directory, entry);
3657
+ let stat: fsSync.BigIntStats;
3658
+ try {
3659
+ stat = fsSync.lstatSync(pathname, { bigint: true });
3660
+ } catch {
3661
+ return false;
3662
+ }
3663
+ if (
3664
+ stat.dev === cleanupTarget.transcriptIdentity.dev &&
3665
+ stat.ino === cleanupTarget.transcriptIdentity.ino
3666
+ )
3667
+ return false;
3668
+ if (stat.isDirectory()) pendingDirectories.push(pathname);
3669
+ }
3670
+ }
3671
+ for (const snapshot of snapshots) {
3672
+ const after = fsSync.lstatSync(snapshot.path, { bigint: true });
3673
+ if (
3674
+ !after.isDirectory() ||
3675
+ after.dev !== snapshot.stat.dev ||
3676
+ after.ino !== snapshot.stat.ino ||
3677
+ after.mtimeNs !== snapshot.stat.mtimeNs ||
3678
+ after.ctimeNs !== snapshot.stat.ctimeNs
3679
+ )
3680
+ return false;
3681
+ }
3682
+ return true;
3683
+ } catch {
3684
+ return false;
3685
+ }
3686
+ };
3687
+ const retainedTranscriptReplayHasNoSideAuthority = retainedTranscriptSidePaths.every(
3688
+ candidate => candidate === undefined,
3689
+ );
3690
+ const retainedTranscriptSideAuthorityIsProvenAbsent =
3691
+ retainedTranscriptSidePaths.every(pathIsAbsent) &&
3692
+ (cleanupTarget.detachedTranscriptPath ? true : retainedTranscriptIdentityIsAbsentFromParent());
3693
+ if (cleanup && !retainedTranscriptReplayHasNoSideAuthority) {
3694
+ if (!retainedTranscriptSideAuthorityIsProvenAbsent) return await publishRetainedTranscriptSideAuthority();
3695
+ cleanupTarget.retainedTranscriptSuccessorPath = undefined;
3696
+ cleanupTarget.retainedTranscriptPlaceholderPath = undefined;
3697
+ cleanupTarget.retainedTranscriptUnknownPath = undefined;
3698
+ preauthorizedCleanup.retainedTranscriptSuccessorPath = undefined;
3699
+ preauthorizedCleanup.retainedTranscriptPlaceholderPath = undefined;
3700
+ preauthorizedCleanup.retainedTranscriptUnknownPath = undefined;
3701
+ }
3702
+ if (cleanup?.phase === "artifacts" && !artifactQuarantineAliasesAreAbsent(true))
3703
+ return await publishChangedArtifactRoot(
3704
+ cleanupTarget.detachedArtifactsPath,
3705
+ "Saved session cleanup is pending in artifacts: another authorized quarantine alias remains.",
3706
+ );
3707
+ if (cleanup?.phase === "transcript" && !artifactQuarantineAliasesAreAbsent(false))
3708
+ return await publishCanonicalArtifactReappearance(
3709
+ preauthorizedCleanup,
3710
+ "Saved session cleanup is pending in artifacts: a planned quarantine alias reappeared before transcript reconciliation.",
3711
+ );
3240
3712
  await broker.ledger.transition(identity, "effect_started", {
3241
3713
  intendedSessionId: id,
3242
3714
  effectMarker: randomUUID(),
@@ -3248,45 +3720,87 @@ async function executeLifecycleResponse(
3248
3720
  });
3249
3721
  let deleted: VerifiedSessionDeleteResult;
3250
3722
  try {
3251
- deleted = await validated.storage.deleteSessionVerified(cleanupTarget);
3723
+ const completedArtifactReplay = cleanup?.phase === "transcript" && cleanup.artifactsRemoved === true;
3724
+ if (completedArtifactReplay && !pathIsAbsent(canonicalArtifactsPath))
3725
+ return await publishCanonicalArtifactReappearance(preauthorizedCleanup);
3726
+ if (
3727
+ completedArtifactReplay &&
3728
+ transcriptCleanupAuthorityIsAbsent() &&
3729
+ retainedTranscriptIdentityIsAbsentFromParent()
3730
+ )
3731
+ return fail(
3732
+ "cleanup_pending",
3733
+ "Saved session cleanup remains pending because transcript authority disappeared without native deletion proof.",
3734
+ preauthorizedCleanup,
3735
+ );
3736
+ else {
3737
+ if (!transcriptParentMatchesPersistedIdentity())
3738
+ return fail(
3739
+ "cleanup_pending",
3740
+ "Saved session cleanup is pending because transcript parent identity changed before exact mutation.",
3741
+ preauthorizedCleanup,
3742
+ );
3743
+ deleted = await validated.storage.deleteSessionVerified(cleanupTarget);
3744
+ }
3252
3745
  } catch (error) {
3253
- if (error instanceof SessionDeleteVerificationError)
3746
+ if (error instanceof SessionDeleteVerificationError) {
3747
+ if (cleanup?.phase === "artifacts")
3748
+ return await publishChangedArtifactRoot(
3749
+ cleanupTarget.detachedArtifactsPath,
3750
+ `Saved session cleanup remains pending in exact artifact authority: ${error.message}`,
3751
+ );
3752
+ if (cleanup?.phase === "transcript" && cleanup.artifactsRemoved === true && error.kind === "artifacts")
3753
+ return await publishCanonicalArtifactReappearance(preauthorizedCleanup);
3254
3754
  return fail(
3255
3755
  "invalid_input",
3256
3756
  `Saved session deletion verification failed (${error.kind}): ${error.message}`,
3257
3757
  );
3758
+ }
3258
3759
  return fail(
3259
3760
  "unavailable",
3260
3761
  `Unable to delete saved session artifacts: ${error instanceof Error ? error.message : String(error)}`,
3261
3762
  );
3262
3763
  }
3263
- const retainedArtifactsRootOnly =
3264
- deleted.kind === "cleanup_pending" &&
3265
- deleted.phase === "artifacts" &&
3266
- deleted.detachedArtifactsPath !== undefined &&
3267
- (() => {
3268
- const snapshot = native.snapshotDirectoryTree(deleted.detachedArtifactsPath);
3269
- if (!snapshot.ok || !snapshot.snapshot) return false;
3270
- const entries = snapshot.snapshot.entries;
3271
- return entries.length === 1 && entries[0]?.relativePath === "" && entries[0].kind === "directory";
3272
- })();
3273
- const retainedArtifactsEvidence =
3274
- retainedArtifactsRootOnly && deleted.kind === "cleanup_pending" && deleted.phase === "artifacts"
3275
- ? {
3276
- detachedArtifactsPath: deleted.detachedArtifactsPath,
3277
- ...(deleted.artifactsIdentity
3278
- ? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
3279
- : {}),
3280
- }
3281
- : {};
3282
- if (deleted.kind === "artifacts_removed" || retainedArtifactsRootOnly) {
3283
- const transcriptPhaseCleanup = {
3764
+ if (
3765
+ deleted.kind === "deleted" &&
3766
+ cleanup?.phase === "transcript" &&
3767
+ cleanup.artifactsRemoved === true &&
3768
+ !pathIsAbsent(canonicalArtifactsPath)
3769
+ )
3770
+ return await publishCanonicalArtifactReappearance(preauthorizedCleanup);
3771
+ if (
3772
+ deleted.kind === "deleted" &&
3773
+ cleanup?.phase === "transcript" &&
3774
+ cleanup.artifactsRemoved === true &&
3775
+ !artifactQuarantineAliasesAreAbsent(false)
3776
+ )
3777
+ return await publishCanonicalArtifactReappearance(
3778
+ preauthorizedCleanup,
3779
+ "Saved session cleanup is pending in artifacts: a planned quarantine alias reappeared during transcript cleanup.",
3780
+ );
3781
+ if (deleted.kind === "artifacts_removed") {
3782
+ if (!artifactQuarantineAliasesAreAbsent(false))
3783
+ return await publishChangedArtifactRoot(
3784
+ cleanupTarget.detachedArtifactsPath,
3785
+ "Saved session cleanup is pending in artifacts: an authorized quarantine alias remains after exact removal.",
3786
+ );
3787
+ const transcriptPhaseCleanup: CleanupEvidence = {
3284
3788
  ...preauthorizedCleanup,
3285
- phase: "transcript" as const,
3789
+ phase: "transcript",
3286
3790
  artifactsRemoved: true,
3287
- ...(retainedArtifactsRootOnly ? retainedArtifactsEvidence : {}),
3791
+ detachedArtifactsPath: undefined,
3792
+ retainedArtifactsSuccessorPath: undefined,
3793
+ retainedArtifactsPlaceholderPath: undefined,
3794
+ retainedArtifactsUnknownPath: undefined,
3795
+ retainedArtifactsSideAuthority: "none",
3288
3796
  ...(preauthorizedCleanup.artifactTree
3289
- ? { artifactTree: { ...preauthorizedCleanup.artifactTree, completed: true as const } }
3797
+ ? {
3798
+ artifactTree: {
3799
+ ...preauthorizedCleanup.artifactTree,
3800
+ detachedPath: undefined,
3801
+ completed: true as const,
3802
+ },
3803
+ }
3290
3804
  : {}),
3291
3805
  };
3292
3806
 
@@ -3298,25 +3812,64 @@ async function executeLifecycleResponse(
3298
3812
  transcriptPhaseCleanup,
3299
3813
  ),
3300
3814
  });
3301
- deleted = await validated.storage.deleteSessionVerified({
3302
- ...cleanupTarget,
3303
- expectedArtifactsIdentity: undefined,
3304
- detachedArtifactsPath: undefined,
3305
- artifactsRemoved: true,
3306
- });
3815
+ if (!pathIsAbsent(canonicalArtifactsPath))
3816
+ return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
3817
+ if (transcriptCleanupAuthorityIsAbsent() && retainedTranscriptIdentityIsAbsentFromParent())
3818
+ return fail(
3819
+ "cleanup_pending",
3820
+ "Saved session cleanup remains pending because transcript authority disappeared without native deletion proof.",
3821
+ transcriptPhaseCleanup,
3822
+ );
3823
+ else {
3824
+ if (!transcriptParentMatchesPersistedIdentity())
3825
+ return fail(
3826
+ "cleanup_pending",
3827
+ "Saved session cleanup is pending because transcript parent identity changed before exact mutation.",
3828
+ transcriptPhaseCleanup,
3829
+ );
3830
+ try {
3831
+ deleted = await validated.storage.deleteSessionVerified({
3832
+ ...cleanupTarget,
3833
+ expectedArtifactsIdentity: undefined,
3834
+ detachedArtifactsPath: undefined,
3835
+ artifactsRemoved: true,
3836
+ });
3837
+ } catch (error) {
3838
+ if (error instanceof SessionDeleteVerificationError && error.kind === "artifacts")
3839
+ return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
3840
+ if (error instanceof SessionDeleteVerificationError)
3841
+ return fail(
3842
+ "invalid_input",
3843
+ `Saved session deletion verification failed (${error.kind}): ${error.message}`,
3844
+ );
3845
+ return fail(
3846
+ "unavailable",
3847
+ `Unable to delete saved session transcript: ${error instanceof Error ? error.message : String(error)}`,
3848
+ );
3849
+ }
3850
+ }
3851
+ if (deleted.kind === "deleted" && !pathIsAbsent(canonicalArtifactsPath))
3852
+ return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
3853
+ if (deleted.kind === "deleted" && !artifactQuarantineAliasesAreAbsent(false))
3854
+ return await publishCanonicalArtifactReappearance(
3855
+ transcriptPhaseCleanup,
3856
+ "Saved session cleanup is pending in artifacts: a planned quarantine alias reappeared during transcript cleanup.",
3857
+ );
3307
3858
  }
3308
- if (
3309
- deleted.kind === "cleanup_pending" &&
3310
- !(
3311
- deleted.phase === "transcript" &&
3312
- deleted.detachedTranscriptPath !== undefined &&
3313
- deleted.detachedTranscriptPath === cleanupTarget.plannedTranscriptPath
3314
- )
3315
- )
3859
+ if (deleted.kind === "deleted" && !artifactQuarantineAliasesAreAbsent(false))
3860
+ return await publishCanonicalArtifactReappearance(
3861
+ preauthorizedCleanup,
3862
+ "Saved session cleanup is pending in artifacts: a planned quarantine alias remains before terminal completion.",
3863
+ );
3864
+ if (deleted.kind === "deleted" && !retainedTranscriptIdentityIsAbsentFromParent())
3865
+ return await publishRetainedTranscriptSideAuthority();
3866
+ const retainedRootArtifactsPlan = durableArtifactsPlan;
3867
+ if (deleted.kind === "cleanup_pending")
3316
3868
  return fail(
3317
3869
  "cleanup_pending",
3318
3870
  `Saved session cleanup is pending in ${deleted.phase}: ${deleted.error.message}`,
3319
3871
  {
3872
+ cleanupReceiptVersion: 1,
3320
3873
  phase: deleted.phase,
3321
3874
  sessionId: validated.target.sessionId,
3322
3875
  sessionsRoot: validated.target.sessionsRoot,
@@ -3324,32 +3877,71 @@ async function executeLifecycleResponse(
3324
3877
  cwd: validated.target.cwd,
3325
3878
  metadataRoot: validated.metadataRoot,
3326
3879
  transcriptIdentity: serializeCleanupIdentity(deleted.transcriptIdentity),
3880
+ transcriptParentIdentity: preauthorizedCleanup.transcriptParentIdentity,
3327
3881
  ...(deleted.phase === "artifacts" && deleted.artifactsIdentity
3328
3882
  ? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
3329
3883
  : {}),
3330
- ...(deleted.phase === "artifacts" && deleted.artifactsIdentity && cleanupTarget.plannedArtifactsPath
3884
+ ...(deleted.phase === "artifacts" && deleted.artifactsIdentity && durableArtifactsPlan
3331
3885
  ? {
3332
3886
  artifactTree: {
3333
3887
  identity: serializeCleanupIdentity(deleted.artifactsIdentity),
3334
3888
  snapshot: deleted.artifactsTree,
3335
- plannedPath: cleanupTarget.plannedArtifactsPath,
3889
+ plannedPath: durableArtifactsPlan,
3336
3890
  ...(deleted.detachedArtifactsPath ? { detachedPath: deleted.detachedArtifactsPath } : {}),
3337
3891
  },
3338
3892
  }
3339
3893
  : {}),
3340
3894
  ...(deleted.phase === "artifacts" ? { detachedArtifactsPath: deleted.detachedArtifactsPath } : {}),
3895
+ ...(deleted.phase === "artifacts" && deleted.retainedSuccessorPath
3896
+ ? { retainedArtifactsSuccessorPath: deleted.retainedSuccessorPath }
3897
+ : {}),
3898
+ ...(deleted.phase === "artifacts" && deleted.retainedPlaceholderPath
3899
+ ? { retainedArtifactsPlaceholderPath: deleted.retainedPlaceholderPath }
3900
+ : {}),
3901
+ ...(deleted.phase === "artifacts" && deleted.retainedUnknownPath
3902
+ ? { retainedArtifactsUnknownPath: deleted.retainedUnknownPath }
3903
+ : {}),
3904
+ ...(deleted.phase === "artifacts"
3905
+ ? {
3906
+ retainedArtifactsSideAuthority: [
3907
+ deleted.retainedSuccessorPath,
3908
+ deleted.retainedPlaceholderPath,
3909
+ deleted.retainedUnknownPath,
3910
+ ].some(candidate => candidate !== undefined)
3911
+ ? ("retained" as const)
3912
+ : ("none" as const),
3913
+ }
3914
+ : {}),
3341
3915
  ...(deleted.phase === "transcript" && deleted.detachedTranscriptPath
3342
3916
  ? { detachedTranscriptPath: deleted.detachedTranscriptPath }
3343
3917
  : {}),
3918
+ ...(deleted.phase === "transcript" && deleted.retainedSuccessorPath
3919
+ ? { retainedTranscriptSuccessorPath: deleted.retainedSuccessorPath }
3920
+ : {}),
3921
+ ...(deleted.phase === "transcript" && deleted.retainedPlaceholderPath
3922
+ ? { retainedTranscriptPlaceholderPath: deleted.retainedPlaceholderPath }
3923
+ : {}),
3924
+ ...(deleted.phase === "transcript" && deleted.retainedUnknownPath
3925
+ ? { retainedTranscriptUnknownPath: deleted.retainedUnknownPath }
3926
+ : {}),
3344
3927
  ...(deleted.phase === "transcript" ? { artifactsRemoved: true } : {}),
3345
3928
  ...(deleted.phase === "transcript" &&
3346
- cleanupTarget.plannedArtifactsPath &&
3929
+ retainedRootArtifactsPlan &&
3347
3930
  cleanupTarget.expectedArtifactsIdentity
3348
3931
  ? {
3349
3932
  artifactTree: {
3350
3933
  identity: serializeCleanupIdentity(cleanupTarget.expectedArtifactsIdentity),
3351
3934
  snapshot: cleanupTarget.expectedArtifactsTree!,
3352
- plannedPath: cleanupTarget.plannedArtifactsPath,
3935
+ plannedPath: retainedRootArtifactsPlan,
3936
+ completed: true as const,
3937
+ },
3938
+ }
3939
+ : {}),
3940
+ ...(deleted.phase === "transcript" && preauthorizedCleanup.artifactTree
3941
+ ? {
3942
+ artifactTree: {
3943
+ ...preauthorizedCleanup.artifactTree,
3944
+ detachedPath: undefined,
3353
3945
  completed: true as const,
3354
3946
  },
3355
3947
  }
@@ -3461,19 +4053,21 @@ function validateLifecycleDeleteMetadataBinding(
3461
4053
  const requestedId = sessionId(input);
3462
4054
  const cwd = lifecycleCwd(input);
3463
4055
  const requestedRoot = stateRoot(input, cwd);
4056
+ const canonicalRequestedRoot = requestedRoot ? canonicalExistingPath(requestedRoot) : undefined;
3464
4057
  if (
3465
4058
  !requestedId ||
3466
4059
  !isCanonicalSessionId(requestedId) ||
3467
4060
  !cwd ||
3468
4061
  !requestedRoot ||
3469
4062
  !hasDefaultStateRoot(cwd, requestedRoot) ||
4063
+ !canonicalRequestedRoot ||
3470
4064
  cleanup.sessionId !== requestedId ||
3471
4065
  !cleanup.metadataRoot ||
3472
- path.resolve(cleanup.metadataRoot) !== requestedRoot
4066
+ canonicalExistingPath(cleanup.metadataRoot) !== canonicalRequestedRoot
3473
4067
  )
3474
4068
  return fail("terminal_uncertain", "Lifecycle delete metadata cleanup does not match the normalized request.");
3475
4069
  const recordedRoot = broker.ledger.get(identity)?.effectIntent?.stateRoot;
3476
- if (recordedRoot && path.resolve(recordedRoot) !== requestedRoot)
4070
+ if (recordedRoot && canonicalExistingPath(recordedRoot) !== canonicalRequestedRoot)
3477
4071
  return fail("terminal_uncertain", "Lifecycle delete metadata cleanup does not match the recorded workspace.");
3478
4072
  return undefined;
3479
4073
  }