chainlesschain 0.165.3 → 0.165.5

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 (214) hide show
  1. package/README.md +151 -29
  2. package/package.json +2 -2
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-BiHNUlbU.js → AIOps-CEeYTeDB.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-D-UFjDRQ.js → ActionButton-BeRUQ3V2.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-DazVsDgW.js → Analytics-DrV718XF.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-DXh9UfNJ.js → AppLayout-Cd2Frt7Q.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-CZ6tsceY.js → Artifacts-CUJMlORU.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-RyxnBr_h.js → Audit-CXJcfT6p.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-BWS51Q6X.js → BackgroundAgents-DOv6Z7H9.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-BRtPcUul.js → Backup-BwdDyBI0.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-CGd4vZYY.js → BaseInput-BmLn4kYH.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-h8ijVwCg.js → Chat-COg8pAW2.js} +4 -4
  14. package/src/assets/web-panel/assets/ChatBubbleRenderer-4gGkdGgA.js +1 -0
  15. package/src/assets/web-panel/assets/{Checkbox-NyF6dPl8.js → Checkbox-BCP4uoU8.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-Ci4OOxr5.js → Codegen-pRt6Y9JI.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-CwTkc9BM.js → Col-DLaprKhS.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-DW_tM82m.js → Community-DyATsz5f.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-DEvHRV4S.js → Compact-BXAaupZK.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-BbjBEEfD.js → Compliance-DpcJBELr.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-10IXBaBa.js → Cowork-DG4EFWyd.js} +2 -2
  22. package/src/assets/web-panel/assets/{Cron-BZkbpd5I.js → Cron-F6Oy5n2P.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-CFQ8L_6v.js → Crosschain-BOdgtalX.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-BDF6AkXe.js → DID-DT1dSkjH.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-CoOdWMi4.js → Dashboard-NBUIqWzC.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-DEsGmu8Y.js → Dropdown-CSpHeMYB.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-BS89SWlK.js → EmailListRenderer-CEG0xISr.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-C1FpCHiI.js → FamilyGuardDashboard-BTZ0BZna.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-BzvpVmLA.js → Federation-BuuCTn2V.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-BeC259PQ.js → FormItemContext-KAVceevk.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-azgG3bmV.js → GenericCardRenderer-DnHpcQvW.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-BmSSowc5.js → Git-C_B_0HwB.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-CTBCSyQZ.js → Governance-DPSZuzuu.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-DA5xyk4a.js → Inference-BV-06V7q.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-D1gPHNig.js → KnowledgeGraph-wpDuupn6.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-BPmLf1AR.js → Logs-Bseca1_I.js} +2 -2
  37. package/src/assets/web-panel/assets/{MarkdownRenderer-SVLOg49H.js → MarkdownRenderer-B4RLp2zs.js} +1 -1
  38. package/src/assets/web-panel/assets/{Marketplace-DtUFcR0E.js → Marketplace-d3kM4DDL.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-DS_kqBv-.js → McpTools-DQ_xZ23i.js} +3 -3
  40. package/src/assets/web-panel/assets/{Memory-Fvh7XL7V.js → Memory-CRIeYO4L.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-C7itAP45.js → MobileBridge-BsllIa9l.js} +1 -1
  42. package/src/assets/web-panel/assets/MobileProjects-BUuQy30X.js +1 -0
  43. package/src/assets/web-panel/assets/{Mtc-B57MiNLK.js → Mtc-BOFDIzrC.js} +6 -6
  44. package/src/assets/web-panel/assets/{MtcAudit-BXfCUcM7.js → MtcAudit-BQg-vyvG.js} +5 -5
  45. package/src/assets/web-panel/assets/{Multisig-yaJ-ZcF6.js → Multisig-Bo1jrPdN.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-CoaUCKrO.js → NLProgramming-Bmod8alw.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BKLM_65R.js → Notes-CGGFL8w-.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-DoDtK9uZ.js → NotificationSettings-BsZvCJxC.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-BJ28TGr-.js → OrderTableRenderer-C1-Quo_h.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-MuGsdoIb.js → Organization-BjOeDYOi.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-BLUN9J4R.js → Overflow-DwVV3l5A.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-DWuoBUPd.js → P2P-Cu3DQT9W.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-CUi37YPj.js → PdhVaultBrowser-Cg0gR7dH.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-BYQg2pcr.js → Permissions-CsAXxRN5.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-CAxoV2xu.js → PersonalDataHub-C-nNmnYp.js} +6 -6
  56. package/src/assets/web-panel/assets/{Pipeline-B33wGvIO.js → Pipeline-BOnW7nNc.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-A8yXTORP.js → Privacy-ClLSyGKt.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-D49I2I1O.js → ProjectInit-DuLvjjq7.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-Ci73jJDw.js → ProjectSettings-DQYYbDD9.js} +2 -2
  60. package/src/assets/web-panel/assets/{Projects-CF-yD6Cn.js → Projects-Drw8MBGK.js} +1 -1
  61. package/src/assets/web-panel/assets/Providers-ckqHQe6K.js +1 -0
  62. package/src/assets/web-panel/assets/{QrScannerModal-Brza0ykJ.js → QrScannerModal-DW5BXk-G.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DFydhjxL.js → QuickAsk-DDLnhNTp.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-DLF8_NhA.js → Recommend-PJQ2C91U.js} +1 -1
  65. package/src/assets/web-panel/assets/{RemoteSession-dyuStW3x.js → RemoteSession-s6wmda34.js} +3 -3
  66. package/src/assets/web-panel/assets/{Reputation-5F3tA6yw.js → Reputation-DC2sb-I3.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-Ca2A57S5.js → Row-hPGk1eGj.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-B0bxBkq_.js → RssFeed-RIvfZql0.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-CFNvHalZ.js → Search-DFg1G1eR.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-DSoH3Hav.js → Security-D6H5p8Dp.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-BHpfEmBQ.js → Services-nxYiFV_w.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-Dsh-QNmn.js → Skeleton-BmgFNAdB.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-BhT1NkSU.js → Skills-CsaaD2av.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-DY9snPeJ.js → Sla-yM5y9yvK.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-CWaX0_Cx.js → SpeechSettings-DPy1XMYh.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-B690y_TC.js → SyncSettings-CFVhEyyr.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-2N-w3zWd.js → Tasks-DdO02Inn.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-BjwQOQYC.js → Templates-Eo2d8Llu.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-CUqHErxY.js → Tenant-ChESOXgy.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-C13rXvNy.js → Terminal-BJ7oDxTI.js} +2 -2
  81. package/src/assets/web-panel/assets/TimelineRenderer-hWX0fUvz.js +1 -0
  82. package/src/assets/web-panel/assets/{Tokens-BXHLGXYC.js → Tokens-DVBZQnd-.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-1e5i65o3.js → Trigger-D89cQfVq.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-FLGqNHs5.js → Trust-BIVSb4-V.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-BbaR_LJB.js → UkeySign-B58gWopl.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-NOp4RIov.js → VideoEditing-DhHI7bmG.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-B7DW7bG4.js → Wallet-C4FheWS_.js} +3 -3
  88. package/src/assets/web-panel/assets/{WebAuthn-BptSUWix.js → WebAuthn-DnPRq-Bx.js} +5 -5
  89. package/src/assets/web-panel/assets/{WorkflowEditor-CAJnDOXS.js → WorkflowEditor-BerRPjpJ.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-I8LgG1-Z.js → chat-BzZ52iKr.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-CWe7SOVD.js → colors-BI_E4BAn.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-Cx3u3CA3.js → compact-item-d50mr3yk.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-CgD5ArwY.js → createContext-C6FsQ1IH.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-CagEcIex.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-BfQWiXN8.js → hasIn-DLZEMgLP.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BuEQSorC.js → index-9H_M2jju.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BiI-xPRH.js → index-A5CDWP7y.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DAVy98u7.js → index-AHr7ryWf.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BUAR7RY-.js → index-B81mo1Et.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DlIyt1qF.js → index-BAE3hwev.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-Dcdm_Z2c.js → index-BB9SqVSt.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CWZ3REg6.js → index-BRGx3w6T.js} +3 -3
  103. package/src/assets/web-panel/assets/{index-B6yChzt1.js → index-BTpIOTfR.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DeizoQVZ.js → index-BZjjdQ62.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-e8BM1yY1.js → index-BcSUl9fA.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CNYFQxAW.js → index-BeM4hfht.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DGMYTN5W.js → index-Bmr_vxXD.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BREnPKGo.js → index-BoNZlzhQ.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-B1cX4Rxu.js → index-C3k8kevG.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-3iKP9OTJ.js → index-CLoTQr0W.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-C1UjHdgZ.js → index-CO8DAF7U.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Cy_cCJlr.js → index-CRN5TRTQ.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-C0nDKhWv.js → index-CUaEHfQ0.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-DslTuqro.js → index-CVzuawCX.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CAUnl4Jv.js → index-CdKt9fOd.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CMu16vPC.js → index-CgkoH3_g.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-C-rIKccl.js → index-CjuKZBqJ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bx7BNIRH.js → index-Co7s9LsB.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-Doc0Ftpd.js → index-D-aBumtY.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Cd79NQAx.js → index-DE0V6cp-.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-V-kIHzAV.js → index-DY4j5zkF.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Bi24S7Dw.js → index-Dec0Kf_Z.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DCJEY2T0.js → index-DoFtCVZ7.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C0ztiqtB.js → index-DqkbHHg8.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CcX4FSPf.js → index-DsscwxPE.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CdRkCwm4.js → index-GXE0D8Nx.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DmgtY58H.js → index-IPkwSAW_.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CQDKYbnv.js → index-KNGgO4Wm.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-87gMxqhN.js → index-Pvf30m8Q.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DmrdUwzU.js → index-Q5Tn735s.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DFb-XIKy.js → index-TWG-_D1M.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DsQcURkA.js → index-Y2-q7QLm.js} +1 -1
  133. package/src/assets/web-panel/assets/index-pzT9PpOU.js +1 -0
  134. package/src/assets/web-panel/assets/index-z1kFadaX.js +1 -0
  135. package/src/assets/web-panel/assets/{initDefaultProps-DiBszVdG.js → initDefaultProps-D-4Hzgej.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-BGIFFozv.js → motion-D4Wl4xU7.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-D5W7mp7B.js → move-Cd5eQvL0.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-JFIlxJ7F.js → mtc-parser-B4ilslCW.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-CQ3o6LIU.js → omit-Ckarx-tf.js} +1 -1
  140. package/src/assets/web-panel/assets/parsers-YNLm7sQ0.js +4 -0
  141. package/src/assets/web-panel/assets/{pickAttrs-R8MIlXo3.js → pickAttrs-QJ0bpCmc.js} +1 -1
  142. package/src/assets/web-panel/assets/{placementArrow-sW7ylSky.js → placementArrow-CKHm-tq8.js} +1 -1
  143. package/src/assets/web-panel/assets/{responsiveObserve-Cnl39Vbz.js → responsiveObserve-CV6a_w_Y.js} +1 -1
  144. package/src/assets/web-panel/assets/{slide-sn8tFd14.js → slide-mHpeQFgk.js} +1 -1
  145. package/src/assets/web-panel/assets/{statusUtils-IKKY_QxZ.js → statusUtils-DIaQNQWA.js} +1 -1
  146. package/src/assets/web-panel/assets/{styleChecker-BNpV4Nqs.js → styleChecker-PJpzavV8.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFlexGapSupport-BWyT_eoU.js → useFlexGapSupport-BH7zTw8S.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFs-D09p6_U9.js → useFs-BDNG3JDd.js} +1 -1
  149. package/src/assets/web-panel/assets/{usePersonalDataHub-CzW3TkJZ.js → usePersonalDataHub-CY2VWdUg.js} +1 -1
  150. package/src/assets/web-panel/assets/{vnode-3s3WoAKB.js → vnode-D0kw9jhz.js} +1 -1
  151. package/src/assets/web-panel/assets/{zoom-Cjy_673s.js → zoom-B5g7TN7r.js} +1 -1
  152. package/src/assets/web-panel/index.html +1 -1
  153. package/src/command-help-index.json +3 -3
  154. package/src/commands/agent.js +105 -35
  155. package/src/commands/artifacts.js +598 -33
  156. package/src/commands/cowork.js +200 -5
  157. package/src/commands/session-budget.js +155 -0
  158. package/src/commands/session-location.js +968 -1
  159. package/src/commands/session.js +23 -2
  160. package/src/data/changelog.json +28 -2
  161. package/src/gateways/ws/artifact-protocol.js +77 -13
  162. package/src/gateways/ws/message-dispatcher.js +57 -0
  163. package/src/gateways/ws/session-protocol.js +391 -24
  164. package/src/gateways/ws/ws-agent-handler.js +191 -14
  165. package/src/gateways/ws/ws-server.js +14 -3
  166. package/src/gateways/ws/ws-session-gateway.js +101 -5
  167. package/src/harness/jsonl-session-store.js +1210 -0
  168. package/src/lib/agent-team/team-runner.js +43 -13
  169. package/src/lib/artifact-access-ledger.js +462 -0
  170. package/src/lib/artifact-cleanup-ledger.js +861 -0
  171. package/src/lib/artifact-deletion-ledger.js +678 -0
  172. package/src/lib/artifact-recovery.js +970 -0
  173. package/src/lib/artifact-store.js +256 -51
  174. package/src/lib/artifact-workbench-projection.js +171 -0
  175. package/src/lib/command-lifecycle-report.js +8 -4
  176. package/src/lib/cowork-adapter.js +9 -5
  177. package/src/lib/cowork-task-runner.js +20 -0
  178. package/src/lib/cowork-workflow.js +178 -20
  179. package/src/lib/direct-model-usage.js +62 -22
  180. package/src/lib/dynamic-workflow-runtime.js +3592 -112
  181. package/src/lib/execution-location-result-apply.js +339 -0
  182. package/src/lib/execution-location-result-artifact.js +271 -0
  183. package/src/lib/execution-location-result-review.js +253 -0
  184. package/src/lib/execution-location-result-store.js +331 -0
  185. package/src/lib/execution-location-result.js +617 -0
  186. package/src/lib/execution-location-target.js +240 -3
  187. package/src/lib/managed-tool-checkpoint.js +47 -4
  188. package/src/lib/mcp-stdio-executable-identity.js +16 -0
  189. package/src/lib/mcp-stdio-native-code-policy.js +74 -0
  190. package/src/lib/mcp-stdio-package-materialization.js +12 -3
  191. package/src/lib/process-execution-broker/index.js +112 -0
  192. package/src/lib/process-execution-broker/platform-sandbox.js +57 -3
  193. package/src/lib/process-execution-broker/workspace-transaction.js +29 -0
  194. package/src/lib/session-budget-production-root.js +376 -0
  195. package/src/lib/session-budget-runtime.js +109 -18
  196. package/src/lib/session-budget-usage.js +63 -0
  197. package/src/lib/session-host-snapshot.js +19 -2
  198. package/src/lib/session-projection.js +126 -10
  199. package/src/lib/session-resource-budget.js +766 -105
  200. package/src/lib/sub-agent-context.js +69 -22
  201. package/src/lib/web-ui-server.js +12 -1
  202. package/src/repl/agent-repl.js +237 -16
  203. package/src/runtime/agent-core.js +135 -0
  204. package/src/runtime/headless-runner.js +135 -46
  205. package/src/runtime/headless-stream.js +133 -25
  206. package/src/runtime/policies/agent-policy.js +10 -0
  207. package/src/assets/web-panel/assets/ChatBubbleRenderer-D7tcdDS9.js +0 -1
  208. package/src/assets/web-panel/assets/MobileProjects-DGbP36-3.js +0 -1
  209. package/src/assets/web-panel/assets/Providers-D-etEhg5.js +0 -1
  210. package/src/assets/web-panel/assets/TimelineRenderer-CpbWcya_.js +0 -1
  211. package/src/assets/web-panel/assets/devWarning-DLv_872g.js +0 -1
  212. package/src/assets/web-panel/assets/index-CdiGCibe.js +0 -1
  213. package/src/assets/web-panel/assets/index-D_0z-gyz.js +0 -1
  214. package/src/assets/web-panel/assets/parsers-UEvh_ShA.js +0 -4
@@ -0,0 +1,970 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { TextDecoder } from "node:util";
5
+ import { canonicalJson } from "./scheduler-kernel/contract.js";
6
+ import {
7
+ readArtifactCleanupLedger,
8
+ settleArtifactCleanup,
9
+ } from "./artifact-cleanup-ledger.js";
10
+ import {
11
+ readArtifactDeletionLedger,
12
+ settleArtifactDeletion,
13
+ } from "./artifact-deletion-ledger.js";
14
+ import { withFileLock } from "./with-file-lock.js";
15
+ import {
16
+ sameFileStatIdentity,
17
+ samePathHandleFileIdentity,
18
+ withTrustedFileParentSync,
19
+ } from "./secure-file-identity.js";
20
+
21
+ export const ARTIFACT_RECOVERY_PLAN_SCHEMA = "cc-artifact-recovery-plan/v1";
22
+ export const ARTIFACT_ORPHAN_GC_EVENT_SCHEMA =
23
+ "cc-artifact-orphan-gc-settlement/v1";
24
+ export const ARTIFACT_ORPHAN_GC_LEDGER_SCHEMA =
25
+ "cc-artifact-orphan-gc-settlement-ledger/v1";
26
+
27
+ export const ARTIFACT_RECOVERY_DECISIONS = Object.freeze([
28
+ "retry",
29
+ "delete-orphan",
30
+ "defer",
31
+ ]);
32
+
33
+ export const DEFAULT_ARTIFACT_RECOVERY_TIMEOUT_MS = 15 * 60 * 1000;
34
+ export const MAX_ARTIFACT_RECOVERY_FILES = 10_000;
35
+
36
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,255}$/u;
37
+ const SHA256_RE = /^sha256:[a-f0-9]{64}$/u;
38
+ const MAX_LEDGER_BYTES = 16 * 1024 * 1024;
39
+ const MAX_LEDGER_EVENTS = 100_000;
40
+ const MAX_ORPHAN_BYTES = 100 * 1024 * 1024;
41
+
42
+ function digest(domain, value) {
43
+ return `sha256:${createHash("sha256")
44
+ .update(domain, "utf8")
45
+ .update(canonicalJson(value, "artifactRecovery"), "utf8")
46
+ .digest("hex")}`;
47
+ }
48
+
49
+ function safeStoredFile(value) {
50
+ const normalized = String(value || "");
51
+ if (
52
+ !normalized ||
53
+ normalized !== path.basename(normalized) ||
54
+ normalized.includes("..") ||
55
+ normalized.includes("/") ||
56
+ normalized.includes("\\")
57
+ ) {
58
+ throw new Error("artifact recovery stored file is invalid");
59
+ }
60
+ return normalized;
61
+ }
62
+
63
+ function nowMs(options) {
64
+ const value = typeof options.now === "function" ? options.now() : Date.now();
65
+ if (!Number.isFinite(value))
66
+ throw new Error("artifact recovery clock is invalid");
67
+ return value;
68
+ }
69
+
70
+ function timeoutMs(options) {
71
+ const value = Number(
72
+ options.timeoutMs ?? DEFAULT_ARTIFACT_RECOVERY_TIMEOUT_MS,
73
+ );
74
+ if (!Number.isSafeInteger(value) || value < 1) {
75
+ throw new Error("artifact recovery timeout is invalid");
76
+ }
77
+ return value;
78
+ }
79
+
80
+ function eventAgeMs(event, observedAt) {
81
+ const occurredAt = Date.parse(event?.occurredAt || "");
82
+ return Number.isFinite(occurredAt) ? Math.max(0, observedAt - occurredAt) : 0;
83
+ }
84
+
85
+ function stableItemId(kind, authority) {
86
+ return `recovery_${createHash("sha256")
87
+ .update("chainlesschain.artifact.recovery-item.v1\0", "utf8")
88
+ .update(kind, "utf8")
89
+ .update("\0", "utf8")
90
+ .update(canonicalJson(authority, "artifactRecoveryItem"), "utf8")
91
+ .digest("hex")}`;
92
+ }
93
+
94
+ function fileIdentity(stat) {
95
+ return Object.freeze({
96
+ size: Number(stat.size),
97
+ mtimeMs: Math.trunc(Number(stat.mtimeMs)),
98
+ ctimeMs: Math.trunc(Number(stat.ctimeMs)),
99
+ mode: Number(stat.mode),
100
+ nlink: Number(stat.nlink),
101
+ dev: String(stat.dev),
102
+ ino: String(stat.ino),
103
+ });
104
+ }
105
+
106
+ function sameFileIdentity(left, right) {
107
+ return ["size", "mtimeMs", "ctimeMs", "mode", "nlink", "dev", "ino"].every(
108
+ (field) => String(left?.[field]) === String(right?.[field]),
109
+ );
110
+ }
111
+
112
+ function inspectRegularFile(filePath, runtimeFs) {
113
+ let descriptor = null;
114
+ try {
115
+ descriptor = runtimeFs.openSync(
116
+ filePath,
117
+ runtimeFs.constants.O_RDONLY |
118
+ Number(runtimeFs.constants.O_NOFOLLOW || 0),
119
+ );
120
+ const before = runtimeFs.fstatSync(descriptor);
121
+ if (!before.isFile() || Number(before.nlink) !== 1) {
122
+ return Object.freeze({ safe: false, reason: "unsafe-file-identity" });
123
+ }
124
+ if (Number(before.size) > MAX_ORPHAN_BYTES) {
125
+ return Object.freeze({ safe: false, reason: "orphan-size-limit" });
126
+ }
127
+ const body = runtimeFs.readFileSync(descriptor);
128
+ const after = runtimeFs.fstatSync(descriptor);
129
+ const beforeIdentity = fileIdentity(before);
130
+ const afterIdentity = fileIdentity(after);
131
+ if (
132
+ !sameFileIdentity(beforeIdentity, afterIdentity) ||
133
+ body.length !== Number(before.size)
134
+ ) {
135
+ return Object.freeze({ safe: false, reason: "file-identity-drift" });
136
+ }
137
+ return Object.freeze({
138
+ safe: true,
139
+ identity: beforeIdentity,
140
+ contentDigest: `sha256:${createHash("sha256").update(body).digest("hex")}`,
141
+ });
142
+ } catch (error) {
143
+ return Object.freeze({
144
+ safe: false,
145
+ reason:
146
+ error?.code === "ENOENT" ? "file-disappeared" : "file-read-failed",
147
+ });
148
+ } finally {
149
+ if (descriptor !== null) runtimeFs.closeSync(descriptor);
150
+ }
151
+ }
152
+
153
+ function pendingById(events, idField) {
154
+ const states = new Map();
155
+ for (const event of events) {
156
+ const id = event[idField];
157
+ const state = states.get(id) || { prepared: null, terminal: null };
158
+ if (event.phase === "prepared") state.prepared = event;
159
+ if (event.phase === "terminal") state.terminal = event;
160
+ states.set(id, state);
161
+ }
162
+ return [...states.values()].filter(
163
+ (state) => state.prepared && !state.terminal,
164
+ );
165
+ }
166
+
167
+ function recoveryItem(kind, authority, details) {
168
+ return Object.freeze({
169
+ itemId: stableItemId(kind, authority),
170
+ kind,
171
+ ...details,
172
+ authority: Object.freeze(authority),
173
+ });
174
+ }
175
+
176
+ function inventoryManagedCopies(store, entries, runtimeFs) {
177
+ const filesDir = path.resolve(store.dir, "files");
178
+ runtimeFs.mkdirSync(filesDir, { recursive: true, mode: 0o700 });
179
+ const dirEntries = runtimeFs.readdirSync(filesDir, { withFileTypes: true });
180
+ if (dirEntries.length > MAX_ARTIFACT_RECOVERY_FILES) {
181
+ throw new Error("artifact recovery file inventory exceeds its limit");
182
+ }
183
+ const referenced = new Map();
184
+ const items = [];
185
+ for (const entry of entries) {
186
+ let storedFile;
187
+ try {
188
+ storedFile = safeStoredFile(entry?.file);
189
+ } catch {
190
+ const authority = {
191
+ artifactId: String(entry?.id || ""),
192
+ recordDigest: entry?.recordDigest || null,
193
+ };
194
+ items.push(
195
+ recoveryItem("invalid-index-row", authority, {
196
+ severity: "critical",
197
+ timedOut: true,
198
+ recommendedDecision: "defer",
199
+ automaticallyExecutable: false,
200
+ requiresApproval: true,
201
+ }),
202
+ );
203
+ continue;
204
+ }
205
+ const refs = referenced.get(storedFile) || [];
206
+ refs.push(entry);
207
+ referenced.set(storedFile, refs);
208
+ }
209
+
210
+ const present = new Set();
211
+ for (const dirEntry of dirEntries.sort((left, right) =>
212
+ left.name.localeCompare(right.name),
213
+ )) {
214
+ let storedFile;
215
+ try {
216
+ storedFile = safeStoredFile(dirEntry.name);
217
+ } catch {
218
+ const authority = {
219
+ nameDigest: digest(
220
+ "chainlesschain.artifact.unsafe-name.v1\0",
221
+ dirEntry.name,
222
+ ),
223
+ };
224
+ items.push(
225
+ recoveryItem("unsafe-managed-entry", authority, {
226
+ severity: "critical",
227
+ timedOut: true,
228
+ recommendedDecision: "defer",
229
+ automaticallyExecutable: false,
230
+ requiresApproval: true,
231
+ }),
232
+ );
233
+ continue;
234
+ }
235
+ present.add(storedFile);
236
+ const refs = referenced.get(storedFile) || [];
237
+ if (refs.length > 0) continue;
238
+ const inspected = inspectRegularFile(
239
+ path.join(filesDir, storedFile),
240
+ runtimeFs,
241
+ );
242
+ const authority = {
243
+ storedFile,
244
+ identity: inspected.safe ? inspected.identity : null,
245
+ contentDigest: inspected.safe ? inspected.contentDigest : null,
246
+ inspectionFailure: inspected.safe ? null : inspected.reason,
247
+ };
248
+ items.push(
249
+ recoveryItem(
250
+ inspected.safe ? "orphan-managed-copy" : "unsafe-orphan-managed-copy",
251
+ authority,
252
+ {
253
+ severity: inspected.safe ? "warning" : "critical",
254
+ timedOut: true,
255
+ recommendedDecision: inspected.safe ? "delete-orphan" : "defer",
256
+ automaticallyExecutable: false,
257
+ requiresApproval: true,
258
+ size: inspected.safe ? inspected.identity.size : null,
259
+ },
260
+ ),
261
+ );
262
+ }
263
+
264
+ for (const [storedFile, refs] of referenced) {
265
+ if (present.has(storedFile)) continue;
266
+ for (const entry of refs) {
267
+ const authority = {
268
+ artifactId: String(entry.id || ""),
269
+ artifactSha256: /^[a-f0-9]{64}$/u.test(String(entry.sha256 || ""))
270
+ ? `sha256:${entry.sha256}`
271
+ : null,
272
+ recordDigest: entry.recordDigest || null,
273
+ artifactSessionId: entry.sessionId || null,
274
+ storedFile,
275
+ };
276
+ items.push(
277
+ recoveryItem("missing-managed-copy", authority, {
278
+ severity: "critical",
279
+ timedOut: true,
280
+ recommendedDecision: "defer",
281
+ automaticallyExecutable: false,
282
+ requiresApproval: true,
283
+ }),
284
+ );
285
+ }
286
+ }
287
+ return items;
288
+ }
289
+
290
+ export function buildArtifactRecoveryPlan(store, options = {}) {
291
+ if (!store?.dir || typeof store._withIndexLock !== "function") {
292
+ throw new TypeError("artifact recovery requires an ArtifactStore");
293
+ }
294
+ const runtimeFs = options.fs || fs;
295
+ const observedAtMs = nowMs(options);
296
+ const pendingTimeoutMs = timeoutMs(options);
297
+ // The verified ledger readers require a canonical parent even when no
298
+ // ledger has been created yet. Establish only the managed directories;
299
+ // startup reconciliation remains read-only with respect to artifacts.
300
+ store._ensureDirs();
301
+ const cleanupLedger = (
302
+ options.readArtifactCleanupLedger || readArtifactCleanupLedger
303
+ )(store, options.cleanupOptions || {});
304
+ const deletionLedger = (
305
+ options.readArtifactDeletionLedger || readArtifactDeletionLedger
306
+ )(store, options.deletionOptions || {});
307
+ const orphanGcLedger = (
308
+ options.readArtifactOrphanGcLedger || readArtifactOrphanGcLedger
309
+ )(store, options.orphanGcOptions || {});
310
+ const pendingCleanups = pendingById(cleanupLedger.events, "cleanupId");
311
+ const pendingManagedFiles = new Set(
312
+ pendingCleanups.flatMap((state) =>
313
+ state.prepared.items.map((item) => item.storedFile),
314
+ ),
315
+ );
316
+ const cleanupDeletionIds = new Set(
317
+ pendingCleanups.flatMap((state) =>
318
+ state.prepared.items.map((item) => item.deletionId),
319
+ ),
320
+ );
321
+ const items = [];
322
+ for (const state of pendingCleanups) {
323
+ const prepared = state.prepared;
324
+ const ageMs = eventAgeMs(prepared, observedAtMs);
325
+ const authority = {
326
+ cleanupId: prepared.cleanupId,
327
+ client: prepared.client,
328
+ preparedEventDigest: prepared.eventDigest,
329
+ scopeDigest: prepared.scopeDigest,
330
+ itemCount: prepared.itemCount,
331
+ };
332
+ items.push(
333
+ recoveryItem("pending-cleanup", authority, {
334
+ severity: ageMs >= pendingTimeoutMs ? "critical" : "warning",
335
+ timedOut: ageMs >= pendingTimeoutMs,
336
+ ageMs,
337
+ recommendedDecision: "retry",
338
+ automaticallyExecutable: false,
339
+ requiresApproval: true,
340
+ }),
341
+ );
342
+ }
343
+ for (const state of pendingById(deletionLedger.events, "deletionId")) {
344
+ const prepared = state.prepared;
345
+ pendingManagedFiles.add(prepared.storedFile);
346
+ if (cleanupDeletionIds.has(prepared.deletionId)) continue;
347
+ const ageMs = eventAgeMs(prepared, observedAtMs);
348
+ const authority = {
349
+ deletionId: prepared.deletionId,
350
+ artifactId: prepared.artifactId,
351
+ client: prepared.client,
352
+ reason: prepared.reason,
353
+ preparedEventDigest: prepared.eventDigest,
354
+ artifactSessionId: prepared.artifactSessionId,
355
+ recordDigest: prepared.recordDigest,
356
+ };
357
+ items.push(
358
+ recoveryItem("pending-deletion", authority, {
359
+ severity: ageMs >= pendingTimeoutMs ? "critical" : "warning",
360
+ timedOut: ageMs >= pendingTimeoutMs,
361
+ ageMs,
362
+ recommendedDecision: "retry",
363
+ automaticallyExecutable: false,
364
+ requiresApproval: true,
365
+ }),
366
+ );
367
+ }
368
+
369
+ const pendingOrphanFiles = new Set();
370
+ for (const state of pendingById(orphanGcLedger.events, "adjudicationId")) {
371
+ const prepared = state.prepared;
372
+ pendingOrphanFiles.add(prepared.storedFile);
373
+ const ageMs = eventAgeMs(prepared, observedAtMs);
374
+ const authority = {
375
+ adjudicationId: prepared.adjudicationId,
376
+ preparedEventDigest: prepared.eventDigest,
377
+ originalPlanDigest: prepared.planDigest,
378
+ originalItemId: prepared.itemId,
379
+ storedFile: prepared.storedFile,
380
+ identity: prepared.identity,
381
+ contentDigest: prepared.contentDigest,
382
+ };
383
+ items.push(
384
+ recoveryItem("pending-orphan-gc", authority, {
385
+ severity: ageMs >= pendingTimeoutMs ? "critical" : "warning",
386
+ timedOut: ageMs >= pendingTimeoutMs,
387
+ ageMs,
388
+ recommendedDecision: "retry",
389
+ automaticallyExecutable: false,
390
+ requiresApproval: true,
391
+ }),
392
+ );
393
+ }
394
+
395
+ const inventory = store._withIndexLock(() => {
396
+ const entries = store._readEntries();
397
+ return {
398
+ indexGenerationDigest: digest(
399
+ "chainlesschain.artifact.index-generation.v1\0",
400
+ entries,
401
+ ),
402
+ items: inventoryManagedCopies(store, entries, runtimeFs),
403
+ };
404
+ });
405
+ items.push(
406
+ ...inventory.items.filter(
407
+ (item) =>
408
+ item.kind !== "orphan-managed-copy" ||
409
+ (!pendingOrphanFiles.has(item.authority.storedFile) &&
410
+ !pendingManagedFiles.has(item.authority.storedFile)),
411
+ ),
412
+ );
413
+ items.sort((left, right) => left.itemId.localeCompare(right.itemId));
414
+ const material = {
415
+ schema: ARTIFACT_RECOVERY_PLAN_SCHEMA,
416
+ policy: {
417
+ timeoutMs: pendingTimeoutMs,
418
+ unattendedMutationAllowed: false,
419
+ },
420
+ indexGenerationDigest: inventory.indexGenerationDigest,
421
+ cleanupLedgerHeadDigest: cleanupLedger.headDigest,
422
+ deletionLedgerHeadDigest: deletionLedger.headDigest,
423
+ orphanGcLedgerHeadDigest: orphanGcLedger.headDigest,
424
+ summary: {
425
+ itemCount: items.length,
426
+ criticalCount: items.filter((item) => item.severity === "critical")
427
+ .length,
428
+ warningCount: items.filter((item) => item.severity === "warning").length,
429
+ timedOutCount: items.filter((item) => item.timedOut).length,
430
+ pendingCleanupCount: items.filter(
431
+ (item) => item.kind === "pending-cleanup",
432
+ ).length,
433
+ pendingDeletionCount: items.filter(
434
+ (item) => item.kind === "pending-deletion",
435
+ ).length,
436
+ pendingOrphanGcCount: items.filter(
437
+ (item) => item.kind === "pending-orphan-gc",
438
+ ).length,
439
+ orphanCount: items.filter((item) => item.kind.includes("orphan")).length,
440
+ missingCount: items.filter((item) => item.kind === "missing-managed-copy")
441
+ .length,
442
+ },
443
+ items,
444
+ };
445
+ const digestMaterial = {
446
+ ...material,
447
+ items: material.items.map((item) => {
448
+ const stable = { ...item };
449
+ delete stable.ageMs;
450
+ return stable;
451
+ }),
452
+ };
453
+ return Object.freeze({
454
+ ...material,
455
+ observedAt: new Date(observedAtMs).toISOString(),
456
+ planDigest: digest(
457
+ "chainlesschain.artifact.recovery-plan.v1\0",
458
+ digestMaterial,
459
+ ),
460
+ });
461
+ }
462
+
463
+ function gcLedgerPath(store) {
464
+ return path.join(path.resolve(store.dir), "orphan-gc-settlements.jsonl");
465
+ }
466
+
467
+ function normalizeGcEvent(input, previous = null) {
468
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
469
+ throw new Error("artifact orphan GC event is invalid");
470
+ }
471
+ const expected = [
472
+ "schema",
473
+ "sequence",
474
+ "previousEventDigest",
475
+ "adjudicationId",
476
+ "phase",
477
+ "preparedEventDigest",
478
+ "planDigest",
479
+ "itemId",
480
+ "storedFile",
481
+ "identity",
482
+ "contentDigest",
483
+ "decision",
484
+ "outcome",
485
+ "occurredAt",
486
+ "eventDigest",
487
+ ].sort();
488
+ const actual = Object.keys(input).sort();
489
+ if (
490
+ actual.length !== expected.length ||
491
+ actual.some((key, index) => key !== expected[index])
492
+ ) {
493
+ throw new Error("artifact orphan GC event schema is invalid");
494
+ }
495
+ if (
496
+ input.schema !== ARTIFACT_ORPHAN_GC_EVENT_SCHEMA ||
497
+ !Number.isSafeInteger(Number(input.sequence)) ||
498
+ Number(input.sequence) !== Number(previous?.sequence || 0) + 1 ||
499
+ input.previousEventDigest !== (previous?.eventDigest || null) ||
500
+ !ID_RE.test(String(input.adjudicationId || "")) ||
501
+ !["prepared", "terminal"].includes(input.phase) ||
502
+ !SHA256_RE.test(String(input.planDigest || "")) ||
503
+ !ID_RE.test(String(input.itemId || "")) ||
504
+ input.decision !== "delete-orphan" ||
505
+ !["pending", "deleted", "already-absent"].includes(input.outcome) ||
506
+ !SHA256_RE.test(String(input.contentDigest || "")) ||
507
+ !Number.isFinite(Date.parse(input.occurredAt || ""))
508
+ ) {
509
+ throw new Error("artifact orphan GC event is invalid");
510
+ }
511
+ safeStoredFile(input.storedFile);
512
+ if (
513
+ (input.phase === "prepared" &&
514
+ (input.preparedEventDigest !== null || input.outcome !== "pending")) ||
515
+ (input.phase === "terminal" &&
516
+ (!SHA256_RE.test(String(input.preparedEventDigest || "")) ||
517
+ input.outcome === "pending"))
518
+ ) {
519
+ throw new Error("artifact orphan GC phase is invalid");
520
+ }
521
+ const material = { ...input };
522
+ delete material.eventDigest;
523
+ if (
524
+ input.eventDigest !==
525
+ digest("chainlesschain.artifact.orphan-gc-event.v1\0", material)
526
+ ) {
527
+ throw new Error("artifact orphan GC event digest is invalid");
528
+ }
529
+ return Object.freeze({ ...input, sequence: Number(input.sequence) });
530
+ }
531
+
532
+ function readGcLedgerUnlocked(store, runtimeFs, runtime = undefined) {
533
+ const filePath = gcLedgerPath(store);
534
+ const bytes = withTrustedFileParentSync(
535
+ runtimeFs,
536
+ filePath,
537
+ ({ canonicalPath, parentDevice }) => {
538
+ let before;
539
+ try {
540
+ before = runtimeFs.lstatSync(canonicalPath, { bigint: true });
541
+ } catch (error) {
542
+ if (error?.code === "ENOENT") return Buffer.alloc(0);
543
+ throw error;
544
+ }
545
+ if (
546
+ before.isSymbolicLink() ||
547
+ !before.isFile() ||
548
+ Number(before.nlink) !== 1 ||
549
+ Number(before.size) > MAX_LEDGER_BYTES
550
+ ) {
551
+ throw new Error("artifact orphan GC ledger identity is invalid");
552
+ }
553
+ let descriptor = null;
554
+ try {
555
+ descriptor = runtimeFs.openSync(
556
+ canonicalPath,
557
+ runtimeFs.constants.O_RDONLY |
558
+ Number(runtimeFs.constants.O_NOFOLLOW || 0),
559
+ );
560
+ const opened = runtimeFs.fstatSync(descriptor, { bigint: true });
561
+ if (
562
+ !opened.isFile() ||
563
+ Number(opened.nlink) !== 1 ||
564
+ !samePathHandleFileIdentity(before, opened, parentDevice, runtime)
565
+ ) {
566
+ throw new Error("artifact orphan GC ledger handle is invalid");
567
+ }
568
+ const content = runtimeFs.readFileSync(descriptor);
569
+ const after = runtimeFs.fstatSync(descriptor, { bigint: true });
570
+ if (
571
+ content.length > MAX_LEDGER_BYTES ||
572
+ Number(after.size) !== content.length ||
573
+ !sameFileStatIdentity(opened, after)
574
+ ) {
575
+ throw new Error("artifact orphan GC ledger changed while reading");
576
+ }
577
+ return content;
578
+ } finally {
579
+ if (descriptor !== null) runtimeFs.closeSync(descriptor);
580
+ }
581
+ },
582
+ { runtime },
583
+ );
584
+ if (bytes.length > 0 && bytes.at(-1) !== 0x0a) {
585
+ throw new Error("artifact orphan GC ledger has a truncated tail");
586
+ }
587
+ let decoded;
588
+ try {
589
+ decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
590
+ } catch {
591
+ throw new Error("artifact orphan GC ledger is not strict UTF-8");
592
+ }
593
+ const lines = decoded.split("\n").filter(Boolean);
594
+ if (lines.length > MAX_LEDGER_EVENTS) {
595
+ throw new Error("artifact orphan GC ledger exceeds its event limit");
596
+ }
597
+ const events = [];
598
+ for (const line of lines) {
599
+ let parsed;
600
+ try {
601
+ parsed = JSON.parse(line);
602
+ } catch {
603
+ throw new Error("artifact orphan GC ledger contains invalid JSON");
604
+ }
605
+ events.push(normalizeGcEvent(parsed, events.at(-1) || null));
606
+ }
607
+ return { bytes, events };
608
+ }
609
+
610
+ function gcEvent(material, previous) {
611
+ return normalizeGcEvent(
612
+ {
613
+ ...material,
614
+ eventDigest: digest(
615
+ "chainlesschain.artifact.orphan-gc-event.v1\0",
616
+ material,
617
+ ),
618
+ },
619
+ previous,
620
+ );
621
+ }
622
+
623
+ function appendGcEvent(filePath, event, expectedSize, runtimeFs) {
624
+ let descriptor = null;
625
+ try {
626
+ descriptor = runtimeFs.openSync(
627
+ filePath,
628
+ runtimeFs.constants.O_WRONLY |
629
+ runtimeFs.constants.O_APPEND |
630
+ runtimeFs.constants.O_CREAT |
631
+ Number(runtimeFs.constants.O_NOFOLLOW || 0),
632
+ 0o600,
633
+ );
634
+ const stat = runtimeFs.fstatSync(descriptor);
635
+ if (
636
+ !stat.isFile() ||
637
+ Number(stat.nlink) !== 1 ||
638
+ Number(stat.size) !== expectedSize
639
+ ) {
640
+ throw new Error("artifact orphan GC ledger changed before append");
641
+ }
642
+ runtimeFs.writeFileSync(descriptor, `${JSON.stringify(event)}\n`, "utf8");
643
+ runtimeFs.fsyncSync(descriptor);
644
+ } finally {
645
+ if (descriptor !== null) runtimeFs.closeSync(descriptor);
646
+ }
647
+ }
648
+
649
+ export function readArtifactOrphanGcLedger(store, options = {}) {
650
+ if (!store?.dir)
651
+ throw new TypeError("artifact orphan GC requires an ArtifactStore");
652
+ const events = readGcLedgerUnlocked(
653
+ store,
654
+ options.fs || fs,
655
+ options.runtime,
656
+ ).events;
657
+ const pending = pendingById(events, "adjudicationId");
658
+ return Object.freeze({
659
+ schema: ARTIFACT_ORPHAN_GC_LEDGER_SCHEMA,
660
+ eventCount: events.length,
661
+ preparedCount: events.filter((event) => event.phase === "prepared").length,
662
+ terminalCount: events.filter((event) => event.phase === "terminal").length,
663
+ pendingCount: pending.length,
664
+ headDigest: events.at(-1)?.eventDigest || null,
665
+ events: Object.freeze(events),
666
+ });
667
+ }
668
+
669
+ function settleOrphanGc(store, request, options) {
670
+ const runtimeFs = options.fs || fs;
671
+ const filePath = gcLedgerPath(store);
672
+ runtimeFs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
673
+ return store._withIndexLock(() =>
674
+ (options.withFileLock || withFileLock)(
675
+ filePath,
676
+ () => {
677
+ let { bytes, events } = readGcLedgerUnlocked(
678
+ store,
679
+ runtimeFs,
680
+ options.runtime,
681
+ );
682
+ const requestEvents = events.filter(
683
+ (event) => event.adjudicationId === request.adjudicationId,
684
+ );
685
+ if (requestEvents.length > 2)
686
+ throw new Error("artifact orphan GC settlement is ambiguous");
687
+ let prepared =
688
+ requestEvents.find((event) => event.phase === "prepared") || null;
689
+ const terminal =
690
+ requestEvents.find((event) => event.phase === "terminal") || null;
691
+ if (prepared) {
692
+ if (
693
+ prepared.itemId !== request.itemId ||
694
+ prepared.planDigest !== request.planDigest
695
+ ) {
696
+ throw new Error(
697
+ "artifact orphan GC adjudication id is already bound",
698
+ );
699
+ }
700
+ if (terminal)
701
+ return Object.freeze({
702
+ schema: "cc-artifact-recovery-adjudication/v1",
703
+ adjudicationId: request.adjudicationId,
704
+ itemId: request.itemId,
705
+ planDigest: request.planDigest,
706
+ decision: "delete-orphan",
707
+ settled: true,
708
+ recorded: false,
709
+ mutationPerformed: true,
710
+ gc: terminal,
711
+ });
712
+ } else {
713
+ const item = request.item;
714
+ if (!item || item.kind !== "orphan-managed-copy") {
715
+ throw new Error(
716
+ "artifact recovery item cannot authorize orphan deletion",
717
+ );
718
+ }
719
+ const currentEntries = store._readEntries();
720
+ if (
721
+ currentEntries.some(
722
+ (entry) => String(entry.file || "") === item.authority.storedFile,
723
+ )
724
+ ) {
725
+ throw new Error(
726
+ "orphan managed copy became referenced before preparation",
727
+ );
728
+ }
729
+ const current = inspectRegularFile(
730
+ path.join(
731
+ path.resolve(store.dir, "files"),
732
+ safeStoredFile(item.authority.storedFile),
733
+ ),
734
+ runtimeFs,
735
+ );
736
+ if (
737
+ !current.safe ||
738
+ !sameFileIdentity(current.identity, item.authority.identity) ||
739
+ current.contentDigest !== item.authority.contentDigest
740
+ ) {
741
+ throw new Error("orphan managed copy changed before preparation");
742
+ }
743
+ const previous = events.at(-1) || null;
744
+ const material = {
745
+ schema: ARTIFACT_ORPHAN_GC_EVENT_SCHEMA,
746
+ sequence: (previous?.sequence || 0) + 1,
747
+ previousEventDigest: previous?.eventDigest || null,
748
+ adjudicationId: request.adjudicationId,
749
+ phase: "prepared",
750
+ preparedEventDigest: null,
751
+ planDigest: request.planDigest,
752
+ itemId: request.itemId,
753
+ storedFile: item.authority.storedFile,
754
+ identity: item.authority.identity,
755
+ contentDigest: item.authority.contentDigest,
756
+ decision: "delete-orphan",
757
+ outcome: "pending",
758
+ occurredAt: new Date(request.observedAtMs).toISOString(),
759
+ };
760
+ prepared = gcEvent(material, previous);
761
+ appendGcEvent(filePath, prepared, bytes.length, runtimeFs);
762
+ bytes = Buffer.concat([
763
+ bytes,
764
+ Buffer.from(`${JSON.stringify(prepared)}\n`, "utf8"),
765
+ ]);
766
+ events = [...events, prepared];
767
+ }
768
+
769
+ const currentEntries = store._readEntries();
770
+ if (
771
+ currentEntries.some(
772
+ (entry) => String(entry.file || "") === prepared.storedFile,
773
+ )
774
+ ) {
775
+ throw new Error(
776
+ "orphan managed copy became referenced before deletion",
777
+ );
778
+ }
779
+ const target = path.join(
780
+ path.resolve(store.dir, "files"),
781
+ safeStoredFile(prepared.storedFile),
782
+ );
783
+ let outcome = "already-absent";
784
+ try {
785
+ const inspected = inspectRegularFile(target, runtimeFs);
786
+ if (!inspected.safe) {
787
+ if (inspected.reason !== "file-disappeared") {
788
+ throw new Error(
789
+ `orphan managed copy is unsafe: ${inspected.reason}`,
790
+ );
791
+ }
792
+ } else {
793
+ if (
794
+ !sameFileIdentity(inspected.identity, prepared.identity) ||
795
+ inspected.contentDigest !== prepared.contentDigest
796
+ ) {
797
+ throw new Error("orphan managed copy changed after preparation");
798
+ }
799
+ runtimeFs.rmSync(target, { force: true });
800
+ outcome = "deleted";
801
+ }
802
+ } catch (error) {
803
+ if (error?.code !== "ENOENT") throw error;
804
+ }
805
+ if (typeof options.afterDelete === "function")
806
+ options.afterDelete({ prepared, outcome });
807
+ if (runtimeFs.existsSync(target))
808
+ throw new Error("orphan managed copy remains after deletion");
809
+ const previous = events.at(-1) || null;
810
+ const terminalMaterial = {
811
+ schema: ARTIFACT_ORPHAN_GC_EVENT_SCHEMA,
812
+ sequence: (previous?.sequence || 0) + 1,
813
+ previousEventDigest: previous?.eventDigest || null,
814
+ adjudicationId: prepared.adjudicationId,
815
+ phase: "terminal",
816
+ preparedEventDigest: prepared.eventDigest,
817
+ planDigest: prepared.planDigest,
818
+ itemId: prepared.itemId,
819
+ storedFile: prepared.storedFile,
820
+ identity: prepared.identity,
821
+ contentDigest: prepared.contentDigest,
822
+ decision: prepared.decision,
823
+ outcome,
824
+ occurredAt: new Date(nowMs(options)).toISOString(),
825
+ };
826
+ const settled = gcEvent(terminalMaterial, previous);
827
+ appendGcEvent(filePath, settled, bytes.length, runtimeFs);
828
+ return Object.freeze({
829
+ schema: "cc-artifact-recovery-adjudication/v1",
830
+ adjudicationId: request.adjudicationId,
831
+ itemId: request.itemId,
832
+ planDigest: request.planDigest,
833
+ decision: "delete-orphan",
834
+ settled: true,
835
+ recorded: true,
836
+ mutationPerformed: true,
837
+ gc: settled,
838
+ });
839
+ },
840
+ {
841
+ failIfUnavailable: true,
842
+ timeoutMs: 30_000,
843
+ retryMs: 1,
844
+ maxRetryMs: 8,
845
+ retryJitterMs: 4,
846
+ },
847
+ ),
848
+ );
849
+ }
850
+
851
+ export function adjudicateArtifactRecovery(store, input = {}, options = {}) {
852
+ if (!store?.dir)
853
+ throw new TypeError("artifact recovery requires an ArtifactStore");
854
+ const itemId = String(input.itemId || "");
855
+ const planDigest = String(input.planDigest || "");
856
+ const decision = String(input.decision || "");
857
+ const adjudicationId = String(
858
+ input.adjudicationId ||
859
+ `artifact_adjudication_${randomUUID().replaceAll("-", "")}`,
860
+ );
861
+ if (
862
+ !ID_RE.test(itemId) ||
863
+ !ID_RE.test(adjudicationId) ||
864
+ !SHA256_RE.test(planDigest) ||
865
+ !ARTIFACT_RECOVERY_DECISIONS.includes(decision)
866
+ ) {
867
+ throw new TypeError("artifact recovery adjudication request is invalid");
868
+ }
869
+ const observedAtMs = nowMs(options);
870
+ if (decision === "delete-orphan") {
871
+ const ledger = readArtifactOrphanGcLedger(
872
+ store,
873
+ options.orphanGcOptions || {},
874
+ );
875
+ const existing = ledger.events.filter(
876
+ (event) => event.adjudicationId === adjudicationId,
877
+ );
878
+ if (existing.length > 0) {
879
+ return settleOrphanGc(
880
+ store,
881
+ { itemId, planDigest, adjudicationId, observedAtMs, item: null },
882
+ options,
883
+ );
884
+ }
885
+ const plan = buildArtifactRecoveryPlan(store, {
886
+ ...options,
887
+ now: () => observedAtMs,
888
+ });
889
+ if (plan.planDigest !== planDigest)
890
+ throw new Error("artifact recovery plan changed before adjudication");
891
+ const item = plan.items.find((candidate) => candidate.itemId === itemId);
892
+ return settleOrphanGc(
893
+ store,
894
+ { itemId, planDigest, adjudicationId, observedAtMs, item },
895
+ options,
896
+ );
897
+ }
898
+ const plan = buildArtifactRecoveryPlan(store, {
899
+ ...options,
900
+ now: () => observedAtMs,
901
+ });
902
+ if (plan.planDigest !== planDigest)
903
+ throw new Error("artifact recovery plan changed before adjudication");
904
+ const item = plan.items.find((candidate) => candidate.itemId === itemId);
905
+ if (!item) throw new Error("artifact recovery item is no longer pending");
906
+ if (decision === "defer") {
907
+ return Object.freeze({
908
+ schema: "cc-artifact-recovery-adjudication/v1",
909
+ adjudicationId,
910
+ itemId,
911
+ planDigest,
912
+ decision,
913
+ settled: false,
914
+ mutationPerformed: false,
915
+ });
916
+ }
917
+ if (item.kind === "pending-cleanup") {
918
+ const result = (options.settleArtifactCleanup || settleArtifactCleanup)(
919
+ store,
920
+ { cleanupId: item.authority.cleanupId, client: item.authority.client },
921
+ options.cleanupOptions || {},
922
+ );
923
+ return Object.freeze({
924
+ schema: "cc-artifact-recovery-adjudication/v1",
925
+ adjudicationId,
926
+ itemId,
927
+ planDigest,
928
+ decision,
929
+ settled: result.settled === true,
930
+ mutationPerformed: true,
931
+ result,
932
+ });
933
+ }
934
+ if (item.kind === "pending-deletion") {
935
+ const result = (options.settleArtifactDeletion || settleArtifactDeletion)(
936
+ store,
937
+ {
938
+ deletionId: item.authority.deletionId,
939
+ artifactId: item.authority.artifactId,
940
+ client: item.authority.client,
941
+ reason: item.authority.reason,
942
+ },
943
+ options.deletionOptions || {},
944
+ );
945
+ return Object.freeze({
946
+ schema: "cc-artifact-recovery-adjudication/v1",
947
+ adjudicationId,
948
+ itemId,
949
+ planDigest,
950
+ decision,
951
+ settled: result.settled === true,
952
+ mutationPerformed: true,
953
+ result,
954
+ });
955
+ }
956
+ if (item.kind === "pending-orphan-gc") {
957
+ return settleOrphanGc(
958
+ store,
959
+ {
960
+ itemId: item.authority.originalItemId,
961
+ planDigest: item.authority.originalPlanDigest,
962
+ adjudicationId: item.authority.adjudicationId,
963
+ observedAtMs,
964
+ item: null,
965
+ },
966
+ options,
967
+ );
968
+ }
969
+ throw new Error("artifact recovery item does not support retry");
970
+ }