@gajae-code/coding-agent 0.12.5 → 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 (127) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -1
  3. package/dist/types/eval/backend.d.ts +2 -2
  4. package/dist/types/exec/bash-executor.d.ts +10 -8
  5. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  7. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  8. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  9. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  10. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  11. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  12. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  13. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  14. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  15. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  16. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  18. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  19. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  20. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  21. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  22. package/dist/types/sdk/broker/broker.d.ts +18 -0
  23. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  25. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -3
  26. package/dist/types/sdk/bus/index.d.ts +4 -0
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -2
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  29. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  30. package/dist/types/sdk/host/host.d.ts +23 -2
  31. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  32. package/dist/types/session/agent-session.d.ts +5 -4
  33. package/dist/types/session/artifacts.d.ts +8 -5
  34. package/dist/types/session/attempt-record-store.d.ts +19 -0
  35. package/dist/types/session/client-bridge.d.ts +14 -0
  36. package/dist/types/session/internal/managed-session-storage.d.ts +5 -2
  37. package/dist/types/session/messages.d.ts +4 -1
  38. package/dist/types/session/session-manager.d.ts +6 -4
  39. package/dist/types/session/session-storage.d.ts +8 -0
  40. package/dist/types/session/streaming-output.d.ts +32 -0
  41. package/dist/types/task/receipt.d.ts +2 -0
  42. package/dist/types/task/types.d.ts +178 -0
  43. package/dist/types/tools/bash.d.ts +1 -1
  44. package/dist/types/tools/fetch.d.ts +3 -0
  45. package/dist/types/tools/output-meta.d.ts +37 -5
  46. package/dist/types/tools/tool-result.d.ts +1 -0
  47. package/dist/types/utils/artifact-id.d.ts +2 -0
  48. package/package.json +8 -8
  49. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  50. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  51. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  52. package/src/config/model-profiles.ts +8 -0
  53. package/src/config/settings-schema.ts +17 -3
  54. package/src/cursor.ts +20 -13
  55. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  56. package/src/eval/backend.ts +2 -2
  57. package/src/exec/bash-executor.ts +425 -151
  58. package/src/extensibility/extensions/runner.ts +80 -6
  59. package/src/extensibility/extensions/wrapper.ts +22 -15
  60. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  61. package/src/gjc-runtime/launch-tmux.ts +309 -293
  62. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  63. package/src/gjc-runtime/psmux-detect.ts +52 -6
  64. package/src/gjc-runtime/team-launch.ts +50 -2
  65. package/src/gjc-runtime/team-runtime.ts +716 -251
  66. package/src/gjc-runtime/team-store.ts +25 -7
  67. package/src/gjc-runtime/team-workers.ts +161 -1
  68. package/src/gjc-runtime/tmux-common.ts +16 -4
  69. package/src/gjc-runtime/tmux-gc.ts +37 -5
  70. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  71. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  72. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  73. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  74. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  75. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  76. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  77. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  78. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  79. package/src/internal-urls/agent-protocol.ts +98 -38
  80. package/src/internal-urls/artifact-protocol.ts +28 -17
  81. package/src/internal-urls/docs-index.generated.ts +5 -5
  82. package/src/internal-urls/local-protocol.ts +39 -18
  83. package/src/internal-urls/registry-helpers.ts +62 -0
  84. package/src/modes/acp/acp-agent.ts +51 -13
  85. package/src/modes/acp/acp-client-bridge.ts +154 -11
  86. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  87. package/src/modes/components/plugin-settings.ts +5 -0
  88. package/src/modes/components/settings-selector.ts +4 -0
  89. package/src/modes/controllers/selector-controller.ts +1 -0
  90. package/src/prompts/tools/bash.md +2 -2
  91. package/src/prompts/tools/monitor.md +4 -4
  92. package/src/sdk/acp/adapter.ts +200 -9
  93. package/src/sdk/broker/broker.ts +93 -10
  94. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  95. package/src/sdk/broker/lifecycle.ts +691 -92
  96. package/src/sdk/bus/chat-daemon-control.ts +32 -6
  97. package/src/sdk/bus/index.ts +347 -14
  98. package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
  99. package/src/sdk/bus/telegram-daemon.ts +283 -44
  100. package/src/sdk/bus/topic-registry.ts +133 -10
  101. package/src/sdk/host/host.ts +187 -9
  102. package/src/sdk/host/reverse-leases.ts +249 -42
  103. package/src/sdk/session.ts +7 -6
  104. package/src/session/agent-session.ts +930 -354
  105. package/src/session/artifacts.ts +211 -74
  106. package/src/session/attempt-record-store.ts +106 -0
  107. package/src/session/client-bridge.ts +15 -0
  108. package/src/session/internal/managed-session-scope.ts +252 -67
  109. package/src/session/internal/managed-session-storage.ts +105 -5
  110. package/src/session/messages.ts +32 -7
  111. package/src/session/session-manager.ts +19 -21
  112. package/src/session/session-storage.ts +146 -24
  113. package/src/session/streaming-output.ts +198 -40
  114. package/src/task/index.ts +45 -0
  115. package/src/task/provider-retry-status.ts +4 -1
  116. package/src/task/receipt.ts +4 -0
  117. package/src/task/render.ts +30 -0
  118. package/src/task/types.ts +34 -0
  119. package/src/tools/bash.ts +818 -239
  120. package/src/tools/eval.ts +99 -4
  121. package/src/tools/fetch.ts +77 -15
  122. package/src/tools/find.ts +7 -3
  123. package/src/tools/monitor.ts +7 -1
  124. package/src/tools/output-meta.ts +706 -116
  125. package/src/tools/search.ts +9 -4
  126. package/src/tools/tool-result.ts +1 -0
  127. package/src/utils/artifact-id.ts +6 -0
@@ -692,6 +692,7 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
692
692
  named.isSymbolicLink() ||
693
693
  named.dev !== snapshot.identity.dev ||
694
694
  named.ino !== snapshot.identity.ino ||
695
+ named.nlink !== 1n ||
695
696
  Number(named.size) !== snapshot.identity.size ||
696
697
  named.mtimeNs !== snapshot.identity.mtimeNs
697
698
  )
@@ -706,6 +707,7 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
706
707
  canonicalPath: path.resolve(filePath),
707
708
  sessionId: header.id,
708
709
  ...snapshot.identity,
710
+ nlink: named.nlink,
709
711
  mtimeMs: Number(named.mtimeMs),
710
712
  sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
711
713
  },
@@ -1138,6 +1140,7 @@ function sameCandidate(left: ManagedCandidate, right: ManagedCandidate): boolean
1138
1140
  left.path === right.path &&
1139
1141
  left.identity.dev === right.identity.dev &&
1140
1142
  left.identity.ino === right.identity.ino &&
1143
+ left.identity.nlink === right.identity.nlink &&
1141
1144
  left.identity.size === right.identity.size &&
1142
1145
  left.identity.mtimeNs === right.identity.mtimeNs &&
1143
1146
  left.identity.sha256 === right.identity.sha256
@@ -1150,6 +1153,7 @@ function matchesExpectedResumeIdentity(candidate: ManagedCandidate, expected: Re
1150
1153
  candidate.identity.sessionId === expected.sessionId &&
1151
1154
  candidate.identity.dev === expected.dev &&
1152
1155
  candidate.identity.ino === expected.ino &&
1156
+ candidate.identity.nlink === expected.nlink &&
1153
1157
  candidate.identity.size === expected.size &&
1154
1158
  candidate.identity.mtimeMs === expected.mtimeMs &&
1155
1159
  candidate.identity.mtimeNs === expected.mtimeNs &&
@@ -1364,7 +1368,27 @@ function preparedReceiptMatches(
1364
1368
  }
1365
1369
  }
1366
1370
 
1367
- 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
+ }
1368
1392
 
1369
1393
  function retiredTargets(scope: ManagedScope, pathname: string): readonly RetiredTarget[] | undefined {
1370
1394
  try {
@@ -1385,6 +1409,14 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
1385
1409
  const identity = item.identity;
1386
1410
  if (!identity || typeof identity !== "object" || Array.isArray(identity)) return undefined;
1387
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>;
1388
1420
  if (
1389
1421
  typeof item.path !== "string" ||
1390
1422
  typeof item.sessionId !== "string" ||
@@ -1394,10 +1426,13 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
1394
1426
  typeof fields.canonicalPath !== "string" ||
1395
1427
  typeof fields.dev !== "string" ||
1396
1428
  typeof fields.ino !== "string" ||
1429
+ typeof fields.nlink !== "string" ||
1397
1430
  typeof fields.size !== "number" ||
1398
1431
  typeof fields.mtimeMs !== "number" ||
1399
1432
  typeof fields.mtimeNs !== "string" ||
1400
- typeof fields.sha256 !== "string"
1433
+ typeof fields.sha256 !== "string" ||
1434
+ typeof parentFields.dev !== "string" ||
1435
+ typeof parentFields.ino !== "string"
1401
1436
  )
1402
1437
  return undefined;
1403
1438
  const provenance =
@@ -1416,12 +1451,17 @@ function retiredTargets(scope: ManagedScope, pathname: string): readonly Retired
1416
1451
  canonicalPath: fields.canonicalPath,
1417
1452
  dev: BigInt(fields.dev),
1418
1453
  ino: BigInt(fields.ino),
1454
+ nlink: BigInt(fields.nlink as string),
1419
1455
  size: fields.size,
1420
1456
  mtimeMs: fields.mtimeMs,
1421
1457
  mtimeNs: BigInt(fields.mtimeNs),
1422
1458
  sha256: fields.sha256,
1423
1459
  sessionId: item.sessionId,
1424
1460
  },
1461
+ transcriptParentIdentity: {
1462
+ dev: BigInt(parentFields.dev as string),
1463
+ ino: BigInt(parentFields.ino as string),
1464
+ },
1425
1465
  });
1426
1466
  }
1427
1467
  return targets;
@@ -1498,7 +1538,9 @@ function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean
1498
1538
  type CleanupReceipt = {
1499
1539
  attempt: number;
1500
1540
  target: RetiredTarget;
1541
+ transcriptParentIdentity: { dev: bigint; ino: bigint };
1501
1542
  expectedArtifactsIdentity?: SessionStorageFileIdentity;
1543
+ artifactsAbsentAtAuthorization?: true;
1502
1544
  expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
1503
1545
  detachedArtifactsPath?: string;
1504
1546
  detachedTranscriptPath?: string;
@@ -1538,7 +1580,12 @@ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: Cleanup
1538
1580
  cwd: receipt.target.cwd,
1539
1581
  identity: receipt.target.identity,
1540
1582
  },
1583
+ transcriptParentIdentity: {
1584
+ dev: receipt.transcriptParentIdentity.dev.toString(),
1585
+ ino: receipt.transcriptParentIdentity.ino.toString(),
1586
+ },
1541
1587
  ...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
1588
+ ...(receipt.artifactsAbsentAtAuthorization ? { artifactsAbsentAtAuthorization: true as const } : {}),
1542
1589
  ...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
1543
1590
  ...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
1544
1591
  ...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
@@ -1670,15 +1717,49 @@ function transcriptRootMatchesTarget(pathname: string, target: RetiredTarget): b
1670
1717
  }
1671
1718
  }
1672
1719
 
1673
- 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;
1674
1753
  const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
1675
- const activeTranscriptRoots = new Set(
1676
- [pending.plannedTranscriptPath, pending.detachedTranscriptPath].filter((pathname): pathname is string =>
1677
- isQuarantinePath(target, pathname),
1678
- ),
1679
- );
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>();
1680
1761
  const roots = new Set<string>([
1681
- target.path,
1762
+ ...(includeTranscriptAuthority ? [target.path] : []),
1682
1763
  target.path.slice(0, -6),
1683
1764
  pending.plannedArtifactsPath,
1684
1765
  deterministicRemovalRoot(pending.plannedArtifactsPath),
@@ -1746,7 +1827,52 @@ function cleanupRootsAbsent(tombstone: string, target: RetiredTarget, pending: C
1746
1827
  if (!transcriptRootMatchesTarget(pathname, target)) throw new Error("durability_failed");
1747
1828
  return false;
1748
1829
  }
1749
- 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
+ }
1750
1876
  }
1751
1877
 
1752
1878
  function sameArtifactRootIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
@@ -1757,7 +1883,16 @@ function sameArtifactTreeSnapshot(left: NativeDirectoryTreeSnapshot, right: Nati
1757
1883
  if (left.rootDev !== right.rootDev || left.rootIno !== right.rootIno || left.entries.length !== right.entries.length)
1758
1884
  return false;
1759
1885
  const entryKey = (entry: NativeDirectoryTreeSnapshot["entries"][number]): string =>
1760
- 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
+ ]);
1761
1896
  const leftEntries = left.entries.map(entryKey).sort();
1762
1897
  const rightEntries = right.entries.map(entryKey).sort();
1763
1898
  return leftEntries.every((entry, index) => entry === rightEntries[index]);
@@ -1839,6 +1974,7 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
1839
1974
  (item.kind === "file" || item.kind === "directory") &&
1840
1975
  typeof item.dev === "string" &&
1841
1976
  typeof item.ino === "string" &&
1977
+ typeof item.nlink === "string" &&
1842
1978
  typeof item.size === "string" &&
1843
1979
  typeof item.mtimeNs === "string" &&
1844
1980
  typeof item.ctimeNs === "string" &&
@@ -1886,6 +2022,14 @@ function pendingCleanupReceipt(
1886
2022
  const attempt = record.attempt;
1887
2023
  const recorded = record.target as Record<string, unknown> | undefined;
1888
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");
1889
2033
  if (
1890
2034
  record.schemaVersion !== 2 ||
1891
2035
  record.state !== "cleanup_pending" ||
@@ -1899,9 +2043,20 @@ function pendingCleanupReceipt(
1899
2043
  recorded.cwd !== target.cwd ||
1900
2044
  identity?.dev !== String(target.identity.dev) ||
1901
2045
  identity.ino !== String(target.identity.ino) ||
2046
+ identity.nlink !== String(target.identity.nlink) ||
1902
2047
  identity.size !== target.identity.size ||
1903
2048
  identity.mtimeNs !== String(target.identity.mtimeNs) ||
1904
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())) ||
1905
2060
  !isQuarantinePath(target, record.plannedArtifactsPath) ||
1906
2061
  !isQuarantinePath(target, record.plannedTranscriptPath) ||
1907
2062
  record.plannedArtifactsPath === record.plannedTranscriptPath ||
@@ -1923,6 +2078,8 @@ function pendingCleanupReceipt(
1923
2078
  !isRetainedNativePath(target, record.retainedTranscriptUnknownPath))
1924
2079
  )
1925
2080
  throw new Error("durability_failed");
2081
+ if (record.artifactsAbsentAtAuthorization !== undefined && record.artifactsAbsentAtAuthorization !== true)
2082
+ throw new Error("durability_failed");
1926
2083
  const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
1927
2084
  const expectedArtifactsIdentity = artifact
1928
2085
  ? typeof artifact.dev === "string" &&
@@ -1940,6 +2097,8 @@ function pendingCleanupReceipt(
1940
2097
  : undefined
1941
2098
  : undefined;
1942
2099
  if (artifact && !expectedArtifactsIdentity) throw new Error("durability_failed");
2100
+ if (record.artifactsAbsentAtAuthorization === true && expectedArtifactsIdentity)
2101
+ throw new Error("durability_failed");
1943
2102
  const expectedArtifactsTree =
1944
2103
  record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
1945
2104
  if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
@@ -1958,7 +2117,12 @@ function pendingCleanupReceipt(
1958
2117
  latest = {
1959
2118
  attempt,
1960
2119
  target,
2120
+ transcriptParentIdentity: {
2121
+ dev: BigInt(transcriptParentIdentity.dev as string),
2122
+ ino: BigInt(transcriptParentIdentity.ino as string),
2123
+ },
1961
2124
  expectedArtifactsIdentity,
2125
+ artifactsAbsentAtAuthorization: record.artifactsAbsentAtAuthorization === true ? true : undefined,
1962
2126
  expectedArtifactsTree,
1963
2127
  detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
1964
2128
  detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
@@ -1983,7 +2147,13 @@ function artifactIdentityForCleanup(target: RetiredTarget): SessionStorageFileId
1983
2147
  try {
1984
2148
  const stat = fs.lstatSync(target.path.slice(0, -6), { bigint: true });
1985
2149
  if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("unsafe_artifacts");
1986
- 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
+ };
1987
2157
  } catch (error) {
1988
2158
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
1989
2159
  throw error;
@@ -2005,6 +2175,7 @@ function snapshotArtifactTree(pathname: string): NativeDirectoryTreeSnapshot {
2005
2175
  function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | undefined): CleanupReceipt {
2006
2176
  const attempt = (pending?.attempt ?? 0) + 1;
2007
2177
  const directory = path.dirname(target.path);
2178
+ const transcriptParentIdentity = pending?.transcriptParentIdentity ?? target.transcriptParentIdentity;
2008
2179
  const operation = stableOperationName(target);
2009
2180
  const expectedArtifactsIdentity = pending?.expectedArtifactsIdentity ?? artifactIdentityForCleanup(target);
2010
2181
  const expectedArtifactsTree =
@@ -2014,7 +2185,10 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
2014
2185
  return {
2015
2186
  attempt,
2016
2187
  target,
2188
+ transcriptParentIdentity,
2017
2189
  expectedArtifactsIdentity,
2190
+ artifactsAbsentAtAuthorization:
2191
+ pending?.artifactsAbsentAtAuthorization ?? (expectedArtifactsIdentity ? undefined : true),
2018
2192
  expectedArtifactsTree,
2019
2193
  detachedArtifactsPath: pending?.detachedArtifactsPath,
2020
2194
  retainedArtifactsSuccessorPath: pending?.retainedArtifactsSuccessorPath,
@@ -2407,6 +2581,7 @@ function sameArtifactTree(
2407
2581
  entry.kind,
2408
2582
  entry.dev,
2409
2583
  entry.ino,
2584
+ entry.nlink,
2410
2585
  entry.size,
2411
2586
  entry.mtimeNs,
2412
2587
  entry.ctimeNs,
@@ -2889,11 +3064,8 @@ export async function reconcileManagedTombstones(
2889
3064
  if (
2890
3065
  cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
2891
3066
  cleanupRootsAbsent(tombstone, target, observedPending)
2892
- ) {
2893
- fsyncManagedParent(target.path);
2894
- await publishCleanupCompleted(scope, tombstone, target, lock);
3067
+ )
2895
3068
  continue;
2896
- }
2897
3069
  } else throw error;
2898
3070
  }
2899
3071
  const verified = observedPending ? target : validateCandidateForScope(scope, target);
@@ -2908,13 +3080,16 @@ export async function reconcileManagedTombstones(
2908
3080
  : (observedPending ?? nextCleanupReceipt(target, undefined));
2909
3081
  if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
2910
3082
  await publishCleanupPending(scope, tombstone, active, lock);
3083
+ if (!cleanupTranscriptParentMatches(target, active)) continue;
2911
3084
  let deletion = await new FileSessionStorage().deleteSessionVerified({
2912
3085
  sessionsRoot: scope.sessionsRoot,
2913
3086
  transcriptPath: target.path,
2914
3087
  sessionId: target.sessionId,
2915
3088
  cwd: target.cwd,
2916
3089
  transcriptIdentity: target.identity,
3090
+ transcriptParentIdentity: target.transcriptParentIdentity,
2917
3091
  expectedArtifactsIdentity: active.expectedArtifactsIdentity,
3092
+ artifactsAbsentAtAuthorization: active.artifactsAbsentAtAuthorization,
2918
3093
  expectedArtifactsTree: active.expectedArtifactsTree,
2919
3094
  detachedArtifactsPath:
2920
3095
  active.detachedArtifactsPath ??
@@ -2940,6 +3115,8 @@ export async function reconcileManagedTombstones(
2940
3115
  : {}),
2941
3116
  });
2942
3117
  if (deletion.kind === "artifacts_removed") {
3118
+ if (!cleanupRootsAbsent(tombstone, target, active, false)) continue;
3119
+ if (!cleanupTranscriptParentMatches(target, active)) continue;
2943
3120
  await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
2944
3121
  deletion = await new FileSessionStorage().deleteSessionVerified({
2945
3122
  sessionsRoot: scope.sessionsRoot,
@@ -2947,6 +3124,7 @@ export async function reconcileManagedTombstones(
2947
3124
  sessionId: target.sessionId,
2948
3125
  cwd: target.cwd,
2949
3126
  transcriptIdentity: target.identity,
3127
+ transcriptParentIdentity: target.transcriptParentIdentity,
2950
3128
  plannedArtifactsPath: active.plannedArtifactsPath,
2951
3129
  plannedTranscriptPath: active.plannedTranscriptPath,
2952
3130
  detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
@@ -3010,6 +3188,7 @@ export async function reconcileManagedTombstones(
3010
3188
  );
3011
3189
  continue;
3012
3190
  }
3191
+ if (!cleanupRootsAbsent(tombstone, target, active)) continue;
3013
3192
  fsyncManagedParent(target.path);
3014
3193
  await publishCleanupCompleted(scope, tombstone, target, lock);
3015
3194
  } catch (error) {
@@ -3432,7 +3611,11 @@ export async function deleteManagedSessionCandidate(
3432
3611
  let targets = retiredTargets(scope, tombstone);
3433
3612
  if (!targets) {
3434
3613
  if (!current) throw new Error("source_changed");
3435
- 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[];
3436
3619
  lock.assertOwned();
3437
3620
  try {
3438
3621
  await publishManagedTombstone(
@@ -3447,6 +3630,10 @@ export async function deleteManagedSessionCandidate(
3447
3630
  cwd: target.cwd,
3448
3631
  provenance: target.provenance,
3449
3632
  identity: target.identity,
3633
+ transcriptParentIdentity: {
3634
+ dev: target.transcriptParentIdentity.dev.toString(),
3635
+ ino: target.transcriptParentIdentity.ino.toString(),
3636
+ },
3450
3637
  })),
3451
3638
  },
3452
3639
  lock.assertOwned,
@@ -3462,6 +3649,7 @@ export async function deleteManagedSessionCandidate(
3462
3649
  lock.assertOwned();
3463
3650
  const pending = pendingCleanupReceipt(scope, tombstone, target);
3464
3651
  const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
3652
+ if (cleanupCompleted(scope, tombstone, target)) continue;
3465
3653
  try {
3466
3654
  fs.lstatSync(target.path);
3467
3655
  } catch (error) {
@@ -3470,14 +3658,16 @@ export async function deleteManagedSessionCandidate(
3470
3658
  if (
3471
3659
  cleanupArtifactsRemoved(scope, tombstone, target, observedPending.attempt) &&
3472
3660
  cleanupRootsAbsent(tombstone, target, observedPending)
3473
- ) {
3474
- fsyncManagedParent(target.path);
3475
- await publishCleanupCompleted(scope, tombstone, target, lock);
3476
- continue;
3477
- }
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
+ };
3478
3669
  } else throw error;
3479
3670
  }
3480
- if (cleanupCompleted(scope, tombstone, target)) continue;
3481
3671
  deletedAny = true;
3482
3672
  const verified = observedPending ? target : validateCandidateForScope(scope, target);
3483
3673
  if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
@@ -3491,13 +3681,24 @@ export async function deleteManagedSessionCandidate(
3491
3681
  : (observedPending ?? nextCleanupReceipt(target, undefined));
3492
3682
  if (!observedPending || discoveredDetach || requiresFreshCleanupPlan(observedPending))
3493
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
+ };
3494
3693
  let deletion = await new FileSessionStorage().deleteSessionVerified({
3495
3694
  sessionsRoot: scope.sessionsRoot,
3496
3695
  transcriptPath: target.path,
3497
3696
  sessionId: target.sessionId,
3498
3697
  cwd: target.cwd,
3499
3698
  transcriptIdentity: target.identity,
3699
+ transcriptParentIdentity: target.transcriptParentIdentity,
3500
3700
  expectedArtifactsIdentity: active.expectedArtifactsIdentity,
3701
+ artifactsAbsentAtAuthorization: active.artifactsAbsentAtAuthorization,
3501
3702
  expectedArtifactsTree: active.expectedArtifactsTree,
3502
3703
  detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
3503
3704
  detachedTranscriptPath:
@@ -3517,6 +3718,20 @@ export async function deleteManagedSessionCandidate(
3517
3718
  : {}),
3518
3719
  });
3519
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
+ };
3520
3735
  await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
3521
3736
  deletion = await new FileSessionStorage().deleteSessionVerified({
3522
3737
  sessionsRoot: scope.sessionsRoot,
@@ -3524,6 +3739,7 @@ export async function deleteManagedSessionCandidate(
3524
3739
  sessionId: target.sessionId,
3525
3740
  cwd: target.cwd,
3526
3741
  transcriptIdentity: target.identity,
3742
+ transcriptParentIdentity: target.transcriptParentIdentity,
3527
3743
  plannedArtifactsPath: active.plannedArtifactsPath,
3528
3744
  plannedTranscriptPath: active.plannedTranscriptPath,
3529
3745
  detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
@@ -3549,51 +3765,20 @@ export async function deleteManagedSessionCandidate(
3549
3765
  throw new Error("durability_failed");
3550
3766
  const retry = nextCleanupReceipt(target, active);
3551
3767
  await publishCleanupPending(scope, tombstone, cleanupPendingEvidence(retry, active, deletion), lock);
3552
- if (deletion.phase === "artifacts") {
3553
- // A typed retained artifact root proves durable canonical absence; advance
3554
- // to the transcript phase instead of stalling on the retained evidence.
3555
- await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
3556
- deletion = await new FileSessionStorage().deleteSessionVerified({
3557
- sessionsRoot: scope.sessionsRoot,
3558
- transcriptPath: target.path,
3559
- sessionId: target.sessionId,
3560
- cwd: target.cwd,
3561
- transcriptIdentity: target.identity,
3562
- plannedArtifactsPath: active.plannedArtifactsPath,
3563
- plannedTranscriptPath: active.plannedTranscriptPath,
3564
- detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
3565
- retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
3566
- retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
3567
- retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
3568
- retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
3569
- retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
3570
- retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
3571
- artifactsRemoved: true,
3572
- });
3573
- if (deletion.kind === "cleanup_pending") {
3574
- const followup = nextCleanupReceipt(target, active);
3575
- await publishCleanupPending(
3576
- scope,
3577
- tombstone,
3578
- cleanupPendingEvidence(followup, active, deletion),
3579
- lock,
3580
- );
3581
- if (
3582
- deletion.phase !== "transcript" ||
3583
- deletion.detachedTranscriptPath !== active.plannedTranscriptPath
3584
- )
3585
- return {
3586
- kind: "cleanup_pending",
3587
- tombstonePath: tombstone,
3588
- phase: deletion.phase,
3589
- message:
3590
- "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
3591
- };
3592
- }
3593
- }
3594
- // A typed retained transcript quarantine is durable canonical-absence
3595
- // evidence — never a terminal byte-deletion claim; complete the receipt.
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
+ };
3596
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
+ };
3597
3782
  fsyncManagedParent(target.path);
3598
3783
  await publishCleanupCompleted(scope, tombstone, target, lock);
3599
3784
  }