chainlesschain 0.162.189 → 0.162.197

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 (317) hide show
  1. package/README.md +84 -11
  2. package/bin/chainlesschain.js +7 -52
  3. package/completions/_cc +209 -0
  4. package/completions/cc.bash +18 -0
  5. package/completions/cc.fish +13 -0
  6. package/completions/cc.ps1 +226 -0
  7. package/package.json +10 -2
  8. package/src/assets/web-panel/assets/{AIOps-DbZft8w0.js → AIOps-D3Gu1a5d.js} +1 -1
  9. package/src/assets/web-panel/assets/{ActionButton-DoOZpwfN.js → ActionButton-DrwAoKaz.js} +1 -1
  10. package/src/assets/web-panel/assets/{Analytics-B0J0KLTW.js → Analytics-C7Q-gd_3.js} +3 -3
  11. package/src/assets/web-panel/assets/{AppLayout-D9eIR0qq.js → AppLayout-DgBtae-n.js} +4 -4
  12. package/src/assets/web-panel/assets/{Artifacts-BnZXEVeo.js → Artifacts-pQ6LcA-W.js} +1 -1
  13. package/src/assets/web-panel/assets/{Audit-X1ce4Bjn.js → Audit-DK61tocB.js} +1 -1
  14. package/src/assets/web-panel/assets/{BackgroundAgents-BvWR-y5v.js → BackgroundAgents-BOKgDFRh.js} +1 -1
  15. package/src/assets/web-panel/assets/{Backup-BublSGEd.js → Backup-Bo9G4vTP.js} +1 -1
  16. package/src/assets/web-panel/assets/{BaseInput-CR_77Hso.js → BaseInput-Do_dE9_v.js} +1 -1
  17. package/src/assets/web-panel/assets/{Chat-DixW52tq.js → Chat-COVvZzOL.js} +6 -6
  18. package/src/assets/web-panel/assets/ChatBubbleRenderer-BUUyJ5QO.js +1 -0
  19. package/src/assets/web-panel/assets/{Checkbox-CCv1C87K.js → Checkbox-Z9MJMolt.js} +1 -1
  20. package/src/assets/web-panel/assets/{Codegen-B03Cc3dd.js → Codegen-BVzWKVbf.js} +1 -1
  21. package/src/assets/web-panel/assets/{Col-CA06-6pU.js → Col-C8MnYcbT.js} +1 -1
  22. package/src/assets/web-panel/assets/{Community-Bj881J52.js → Community-DQxqw15l.js} +1 -1
  23. package/src/assets/web-panel/assets/{Compact-UZLTPoaL.js → Compact-BAUmrh3y.js} +1 -1
  24. package/src/assets/web-panel/assets/{Compliance-kGwzRb_0.js → Compliance-jPAkQ3zK.js} +1 -1
  25. package/src/assets/web-panel/assets/{Cowork-KundoYM8.js → Cowork-BBQyxbh8.js} +2 -2
  26. package/src/assets/web-panel/assets/{Cron-BYZyoqDe.js → Cron-W5koq8Z5.js} +2 -2
  27. package/src/assets/web-panel/assets/{Crosschain-BhrrppBX.js → Crosschain-BQhnu5hS.js} +1 -1
  28. package/src/assets/web-panel/assets/{DID-CdC6EguZ.js → DID-DD7PlI18.js} +2 -2
  29. package/src/assets/web-panel/assets/{Dashboard-CcQx3J4y.js → Dashboard-w_ILuwKI.js} +2 -2
  30. package/src/assets/web-panel/assets/{Dropdown-BpijtEzQ.js → Dropdown-qiDUAyAo.js} +1 -1
  31. package/src/assets/web-panel/assets/{EmailListRenderer-DfZjeZTe.js → EmailListRenderer-PDeBcI5H.js} +1 -1
  32. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BFYSf69C.js → FamilyGuardDashboard-SuArscRM.js} +1 -1
  33. package/src/assets/web-panel/assets/{Federation-Okdi3pJm.js → Federation-BCovZaUn.js} +1 -1
  34. package/src/assets/web-panel/assets/{FormItemContext-mH-HebKW.js → FormItemContext-DjlFdVFk.js} +1 -1
  35. package/src/assets/web-panel/assets/{GenericCardRenderer-xivV69x7.js → GenericCardRenderer-DoEZ7fcS.js} +1 -1
  36. package/src/assets/web-panel/assets/{Git-1rh_YBwg.js → Git-CV0-6GPI.js} +2 -2
  37. package/src/assets/web-panel/assets/{Governance-eAXkNxdY.js → Governance-B_SFN50w.js} +1 -1
  38. package/src/assets/web-panel/assets/{Inference-CS6dMVTN.js → Inference-heI0Eo6k.js} +1 -1
  39. package/src/assets/web-panel/assets/{KnowledgeGraph-Bq_TnkvD.js → KnowledgeGraph-Cz5INS5p.js} +1 -1
  40. package/src/assets/web-panel/assets/{Logs-KchHtiHt.js → Logs-BSFxiHkg.js} +2 -2
  41. package/src/assets/web-panel/assets/MarkdownRenderer-1mNozZ8v.js +1 -0
  42. package/src/assets/web-panel/assets/{Marketplace-CsmGcX5X.js → Marketplace-0TWTpE7k.js} +1 -1
  43. package/src/assets/web-panel/assets/{McpTools-BaPFnnyz.js → McpTools-CV4-wBLm.js} +4 -4
  44. package/src/assets/web-panel/assets/{Memory-2m-sUUf7.js → Memory-x5bH5rw5.js} +2 -2
  45. package/src/assets/web-panel/assets/{MobileBridge-D5nVZBRu.js → MobileBridge-DaBmUu9G.js} +1 -1
  46. package/src/assets/web-panel/assets/{MobileProjects-Bo8MLZvT.js → MobileProjects-DqnoFPgR.js} +1 -1
  47. package/src/assets/web-panel/assets/{Mtc-3GDvamGa.js → Mtc-B_zKEBif.js} +4 -4
  48. package/src/assets/web-panel/assets/{MtcAudit-D2zGuZCH.js → MtcAudit-BCfKD2yg.js} +4 -4
  49. package/src/assets/web-panel/assets/{Multisig-BBVQ4t6t.js → Multisig-DgYfj6c_.js} +3 -3
  50. package/src/assets/web-panel/assets/{NLProgramming-BIOCyOGp.js → NLProgramming-Dz09lFUm.js} +1 -1
  51. package/src/assets/web-panel/assets/{Notes-DVwBGpzc.js → Notes-Cuj6ugBE.js} +3 -3
  52. package/src/assets/web-panel/assets/{NotificationSettings-BDnHfIfX.js → NotificationSettings-DK2WXjhg.js} +1 -1
  53. package/src/assets/web-panel/assets/{OrderTableRenderer-C7Ek20ag.js → OrderTableRenderer-D1IAOQoo.js} +1 -1
  54. package/src/assets/web-panel/assets/{Organization-D3SRKaa7.js → Organization-9fqnPvTw.js} +4 -4
  55. package/src/assets/web-panel/assets/{Overflow-C2B8ahyn.js → Overflow-n5xHkhow.js} +1 -1
  56. package/src/assets/web-panel/assets/{P2P-2iwQzG65.js → P2P-B5AUWwH-.js} +2 -2
  57. package/src/assets/web-panel/assets/{PdhVaultBrowser-CoPiWqMN.js → PdhVaultBrowser-CoUDV6dy.js} +4 -4
  58. package/src/assets/web-panel/assets/{Permissions-yV5XNXo5.js → Permissions-DPRmQf_H.js} +4 -4
  59. package/src/assets/web-panel/assets/{PersonalDataHub-uCDJhMUF.js → PersonalDataHub-B37bsbEN.js} +4 -4
  60. package/src/assets/web-panel/assets/{Pipeline-hN7ucMOA.js → Pipeline-CCAuPqu9.js} +1 -1
  61. package/src/assets/web-panel/assets/{Privacy-CgU0ptN4.js → Privacy-B81msZdv.js} +1 -1
  62. package/src/assets/web-panel/assets/{ProjectInit-CfYPiT6j.js → ProjectInit-B-Py2NbF.js} +2 -2
  63. package/src/assets/web-panel/assets/{ProjectSettings-B0A2C4nw.js → ProjectSettings-CYsneiRB.js} +2 -2
  64. package/src/assets/web-panel/assets/{Projects-BE5C0ajn.js → Projects-Y975Mh6R.js} +1 -1
  65. package/src/assets/web-panel/assets/{Providers-CstQPf8W.js → Providers-uO-10aP1.js} +1 -1
  66. package/src/assets/web-panel/assets/{QrScannerModal-Co3z_ynk.js → QrScannerModal-Bj_6gJ0l.js} +1 -1
  67. package/src/assets/web-panel/assets/{QuickAsk-DMTS6pQ6.js → QuickAsk-BgpBCMPE.js} +1 -1
  68. package/src/assets/web-panel/assets/{Recommend-ETw4odUa.js → Recommend-DhUW3YUA.js} +1 -1
  69. package/src/assets/web-panel/assets/RemoteSession-DdeyfbkA.js +5 -0
  70. package/src/assets/web-panel/assets/{Reputation-BTVqQgKq.js → Reputation-fxW421xd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Row-CBKKwwtO.js → Row-De73w1og.js} +1 -1
  72. package/src/assets/web-panel/assets/{RssFeed-BHUQ438I.js → RssFeed-3US_Q_lj.js} +2 -2
  73. package/src/assets/web-panel/assets/{Search-B4NqeUsF.js → Search-DbZ_DU74.js} +1 -1
  74. package/src/assets/web-panel/assets/{Security-BRjoYG5_.js → Security-DoIU1vMF.js} +4 -4
  75. package/src/assets/web-panel/assets/{Services-CxQzu3p3.js → Services-DB_RGqpc.js} +2 -2
  76. package/src/assets/web-panel/assets/{Skeleton-BKzwwmWO.js → Skeleton-DnzLaayC.js} +1 -1
  77. package/src/assets/web-panel/assets/{Skills-DrsFcgth.js → Skills-VzVXHENF.js} +1 -1
  78. package/src/assets/web-panel/assets/{Sla-D21tmXCH.js → Sla-DMfvjCAI.js} +1 -1
  79. package/src/assets/web-panel/assets/{SpeechSettings-CjyjLfp8.js → SpeechSettings-DQQOYWbF.js} +1 -1
  80. package/src/assets/web-panel/assets/{SyncSettings-Cu45-tnx.js → SyncSettings-Bmx3FG1P.js} +2 -2
  81. package/src/assets/web-panel/assets/{Tasks-oMcILYOj.js → Tasks-DOQyfb-2.js} +1 -1
  82. package/src/assets/web-panel/assets/{Templates-Bh-Zcr5R.js → Templates-D6hTRT7c.js} +1 -1
  83. package/src/assets/web-panel/assets/{Tenant-68ns63aI.js → Tenant-CTPQZAzs.js} +1 -1
  84. package/src/assets/web-panel/assets/{Terminal-BLJkFykE.js → Terminal-B7vII3oH.js} +2 -2
  85. package/src/assets/web-panel/assets/{TimelineRenderer-xcrJLq-O.js → TimelineRenderer-BkiFJeXV.js} +1 -1
  86. package/src/assets/web-panel/assets/{Tokens-C6PpFJtf.js → Tokens-BPPdQMNo.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trigger-BaKA7tdM.js → Trigger-BxG8JAEI.js} +1 -1
  88. package/src/assets/web-panel/assets/{Trust-BzhA57Jc.js → Trust-DHD7nMpM.js} +1 -1
  89. package/src/assets/web-panel/assets/{UkeySign-4_U3JkIR.js → UkeySign-B4BJpU47.js} +1 -1
  90. package/src/assets/web-panel/assets/{VideoEditing-162AANxX.js → VideoEditing-CRiGpFx2.js} +1 -1
  91. package/src/assets/web-panel/assets/{Wallet-B1viuql_.js → Wallet-BSDSNiem.js} +4 -4
  92. package/src/assets/web-panel/assets/{WebAuthn-ZVWazhCy.js → WebAuthn-DLTUh5Zv.js} +5 -5
  93. package/src/assets/web-panel/assets/WorkflowEditor-tW2MwjhD.js +1 -0
  94. package/src/assets/web-panel/assets/{chat-DmrHNuwq.js → chat-SgsLQ3FC.js} +1 -1
  95. package/src/assets/web-panel/assets/{colors-DAJ_M-PK.js → colors-BHjjLIKO.js} +1 -1
  96. package/src/assets/web-panel/assets/{compact-item-uIzmYwLs.js → compact-item-VceErdKq.js} +1 -1
  97. package/src/assets/web-panel/assets/{createContext-D_ZmuLwb.js → createContext-BZ332MYl.js} +1 -1
  98. package/src/assets/web-panel/assets/devWarning-DosMaL-k.js +1 -0
  99. package/src/assets/web-panel/assets/{hasIn-CDt_shTL.js → hasIn-CBTjJOjn.js} +1 -1
  100. package/src/assets/web-panel/assets/index-0O5ePnUq.js +1 -0
  101. package/src/assets/web-panel/assets/{index-rXdwUeE3.js → index-1n7xjKDU.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BAvSmJXW.js → index-29wVzi-k.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-C16fYKFD.js → index-B2u26AS3.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-C5WcdJH2.js → index-B40dQn0G.js} +3 -3
  105. package/src/assets/web-panel/assets/{index-CaelyMgF.js → index-B5KAGxNo.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-OzTxFIJP.js → index-BC1xuQgX.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CzXqvMuP.js → index-BETEjuNr.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-clSX4PEc.js → index-BOtvPdHt.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-saf3VNeP.js → index-BPXFLYdW.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DdUMcwRB.js → index-BQSmZGgs.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DfSSy9uy.js → index-BRWhLbWF.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-GcKOHD_c.js → index-BTlc-Bve.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BHvlm0In.js → index-BY1wexYG.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Cvak6Oe2.js → index-BcUenmCX.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CKyd472S.js → index-BeiqkxJG.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DvFhhe6r.js → index-BhXZEUAc.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DBFmcaCQ.js → index-By5jBtkV.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DqDSF2RW.js → index-CLc19alH.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DeGBoLQA.js → index-CMVvxIS8.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-FyplHuNt.js → index-C_B9rFQE.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CeUpKjvi.js → index-CaIS_TNX.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BRa4kJ_S.js → index-CawpdGTW.js} +1 -1
  123. package/src/assets/web-panel/assets/index-ChAzPigF.js +1 -0
  124. package/src/assets/web-panel/assets/{index-QsgfSZrV.js → index-CpumS98o.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BDF6EQBs.js → index-Cr4rjlW5.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-rS3gl_68.js → index-CrUgDQkf.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-D4n1lGTB.js → index-CtjLcCNU.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-ifxmu8SY.js → index-DUkVN794.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BTCXf1t-.js → index-D_SePDWj.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Bbxjfstq.js → index-DaCCy6lq.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-D9OE8B3d.js → index-DdefYBSw.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CG0lY4h3.js → index-DePOeMDM.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-Bx4d6c1k.js → index-DgPvuIn2.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-Z8P4BexE.js → index-FJ3zC9LK.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-ByzGvpR9.js → index-_PHrIO4O.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-DwfvPe-j.js → index-fU7wNHDB.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-CcZ9xJI9.js → index-iYQNU9za.js} +1 -1
  138. package/src/assets/web-panel/assets/{index-CbD_WW_6.js → index-vIXOpcjD.js} +1 -1
  139. package/src/assets/web-panel/assets/{initDefaultProps-pmfaD-aK.js → initDefaultProps-D1oZNz0D.js} +1 -1
  140. package/src/assets/web-panel/assets/{motion-CaQ2CpYF.js → motion-C8k6doMO.js} +1 -1
  141. package/src/assets/web-panel/assets/{move-CkKTqNbI.js → move-DJ4qSbYt.js} +1 -1
  142. package/src/assets/web-panel/assets/{mtc-parser-Dd4xsgRI.js → mtc-parser-CdpJD85j.js} +1 -1
  143. package/src/assets/web-panel/assets/{omit-BFuA5YEs.js → omit-B0V4slKA.js} +1 -1
  144. package/src/assets/web-panel/assets/{pickAttrs-DDfxOAJc.js → pickAttrs-Di5NiW1F.js} +1 -1
  145. package/src/assets/web-panel/assets/{placementArrow-Be595JCp.js → placementArrow-B5OMHgEM.js} +1 -1
  146. package/src/assets/web-panel/assets/{responsiveObserve-7byljUM3.js → responsiveObserve-C6U9IWql.js} +1 -1
  147. package/src/assets/web-panel/assets/{slide-CTWfOTaY.js → slide-BMyTH3rN.js} +1 -1
  148. package/src/assets/web-panel/assets/{statusUtils-CNZOuIxM.js → statusUtils-C7d6BG7Z.js} +1 -1
  149. package/src/assets/web-panel/assets/{styleChecker-DwOcBV78.js → styleChecker-CR50QnQy.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFlexGapSupport-B6MNwAO9.js → useFlexGapSupport-DBEeoMb8.js} +1 -1
  151. package/src/assets/web-panel/assets/{useFs-CzyQjH9r.js → useFs-3r099cjv.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-Dxh3KiVA.js → usePersonalDataHub-C5IYNdTK.js} +1 -1
  153. package/src/assets/web-panel/assets/{vnode-DVfZiS5N.js → vnode-DmEOLRHf.js} +1 -1
  154. package/src/assets/web-panel/assets/{zoom-BYme7fbY.js → zoom-DnpUiVCB.js} +1 -1
  155. package/src/assets/web-panel/index.html +1 -1
  156. package/src/cli-help.js +181 -0
  157. package/src/command-help-index.json +181 -0
  158. package/src/command-manifest.json +1271 -175
  159. package/src/command-surface-policy.js +312 -0
  160. package/src/commands/agent.js +76 -33
  161. package/src/commands/artifacts.js +400 -2
  162. package/src/commands/background-session.js +41 -3
  163. package/src/commands/checkpoint-restore-recovery.js +1476 -0
  164. package/src/commands/checkpoint.js +947 -22
  165. package/src/commands/compact.js +101 -25
  166. package/src/commands/config.js +506 -20
  167. package/src/commands/doctor.js +4 -1
  168. package/src/commands/hub.js +1 -1
  169. package/src/commands/mcp.js +688 -24
  170. package/src/commands/pack.js +16 -2
  171. package/src/commands/plugin.js +20 -12
  172. package/src/commands/session-mcp-recovery.js +218 -0
  173. package/src/commands/session.js +470 -110
  174. package/src/commands/setup.js +36 -10
  175. package/src/commands/skill.js +181 -110
  176. package/src/commands/status.js +21 -5
  177. package/src/commands/update.js +68 -6
  178. package/src/constants.js +11 -0
  179. package/src/data/changelog.json +14 -1
  180. package/src/gateways/ws/action-protocol.js +10 -0
  181. package/src/gateways/ws/session-protocol.js +555 -27
  182. package/src/gateways/ws/ws-agent-handler.js +541 -14
  183. package/src/gateways/ws/ws-session-gateway.js +129 -22
  184. package/src/gateways/ws/ws-session-state.js +431 -0
  185. package/src/harness/background-task-manager.js +233 -36
  186. package/src/harness/jsonl-session-store.js +2551 -167
  187. package/src/harness/mcp-client.js +802 -61
  188. package/src/harness/prompt-compressor.js +227 -27
  189. package/src/harness/session-index.js +89 -38
  190. package/src/harness/session-list-index.js +254 -0
  191. package/src/harness/structured-handoff.js +410 -0
  192. package/src/lazy-dispatch.js +747 -84
  193. package/src/lib/__tests__/logger.test.js +50 -2
  194. package/src/lib/advisor-runtime.js +1047 -0
  195. package/src/lib/agent-core.js +1 -0
  196. package/src/lib/agent-sandbox.js +120 -26
  197. package/src/lib/agent-session-export.js +20 -0
  198. package/src/lib/agent-team/team-runner.js +230 -8
  199. package/src/lib/artifact-store.js +131 -8
  200. package/src/lib/background-agent-supervisor.js +185 -13
  201. package/src/lib/background-interaction-journal.js +37 -9
  202. package/src/lib/background-launch-profile.js +1058 -0
  203. package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
  204. package/src/lib/checkpoint-restore-orchestrator.js +987 -0
  205. package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
  206. package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
  207. package/src/lib/checkpoint-restore-recovery.js +884 -0
  208. package/src/lib/checkpoint-restore-retention.js +126 -0
  209. package/src/lib/checkpoint-restore-saga.js +5079 -0
  210. package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
  211. package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
  212. package/src/lib/checkpoint-store.js +1688 -97
  213. package/src/lib/checkpoint-timeline-authority.js +522 -0
  214. package/src/lib/checkpoint-timeline.js +642 -0
  215. package/src/lib/config-keys.js +18 -119
  216. package/src/lib/config-manager.js +446 -27
  217. package/src/lib/config-redaction.js +57 -0
  218. package/src/lib/config-schema.js +853 -0
  219. package/src/lib/cowork-mcp-tools.js +5 -0
  220. package/src/lib/cowork-task-runner.js +458 -63
  221. package/src/lib/delivery-coordinator.js +1234 -0
  222. package/src/lib/delivery-evidence.js +450 -0
  223. package/src/lib/doctor-checkup.js +199 -14
  224. package/src/lib/executable-path.js +34 -0
  225. package/src/lib/fatal-handler.js +16 -0
  226. package/src/lib/file-checkpoint.js +4909 -186
  227. package/src/lib/file-lines.js +125 -0
  228. package/src/lib/headless-config-command.js +16 -5
  229. package/src/lib/hook-runner.cjs +139 -38
  230. package/src/lib/hooks-v2-workspace-context.js +10 -0
  231. package/src/lib/impacted-gate-selector.js +424 -0
  232. package/src/lib/interaction-adapter.js +62 -0
  233. package/src/lib/jsonl-session-store.js +1 -35
  234. package/src/lib/logger.js +66 -14
  235. package/src/lib/mcp-call-ledger-store.js +1069 -0
  236. package/src/lib/mcp-call-ledger.js +747 -0
  237. package/src/lib/mcp-client.js +5 -0
  238. package/src/lib/mcp-conflict-scheduler.js +468 -0
  239. package/src/lib/mcp-effect-contract.js +102 -0
  240. package/src/lib/mcp-headers-helper-trust.js +145 -0
  241. package/src/lib/mcp-headers-helper.js +616 -0
  242. package/src/lib/mcp-host-recovery-runtime.js +131 -0
  243. package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
  244. package/src/lib/mcp-recovery-adjudication.js +443 -0
  245. package/src/lib/output-context.js +86 -0
  246. package/src/lib/packer/file-identity.js +170 -0
  247. package/src/lib/packer/index.js +3 -0
  248. package/src/lib/packer/native-update-state.js +277 -0
  249. package/src/lib/packer/pack-update-applier.js +1983 -136
  250. package/src/lib/packer/pack-update-checker.js +34 -0
  251. package/src/lib/packer/pack-update-downloader.js +875 -86
  252. package/src/lib/packer/pack-update-signature.js +86 -0
  253. package/src/lib/packer/pkg-config-generator.js +6 -0
  254. package/src/lib/paths.js +65 -10
  255. package/src/lib/plan-mode.js +831 -14
  256. package/src/lib/plan-persistence.js +540 -0
  257. package/src/lib/plugin-runtime/hooks.js +227 -69
  258. package/src/lib/plugin-runtime/plugin-options.js +189 -44
  259. package/src/lib/pr-automation-policy.js +146 -8
  260. package/src/lib/process-execution-broker/credential-agent.js +7 -0
  261. package/src/lib/process-execution-broker/index.js +53 -8
  262. package/src/lib/process-execution-broker/workspace-transaction.js +976 -73
  263. package/src/lib/project-instructions.js +165 -29
  264. package/src/lib/project-mcp-trust.js +67 -0
  265. package/src/lib/public-ide-capability-manifest.js +278 -0
  266. package/src/lib/public-ide-capability-validation.js +351 -0
  267. package/src/lib/secret-store.js +267 -43
  268. package/src/lib/secure-fs.js +1089 -0
  269. package/src/lib/security-audit.js +58 -0
  270. package/src/lib/service-manager.js +34 -13
  271. package/src/lib/session-budget-runtime.js +1452 -0
  272. package/src/lib/session-host-snapshot.js +388 -0
  273. package/src/lib/session-message-provenance.js +429 -0
  274. package/src/lib/session-projection.js +804 -0
  275. package/src/lib/session-resource-budget.js +1495 -0
  276. package/src/lib/settings-hooks.cjs +138 -22
  277. package/src/lib/settings-loader.cjs +25 -2
  278. package/src/lib/side-effect-ledger-store.js +45 -23
  279. package/src/lib/skill-execution-identity.js +217 -0
  280. package/src/lib/skill-loader.js +299 -18
  281. package/src/lib/sub-agent-context.js +236 -28
  282. package/src/lib/sub-agent-registry.js +5 -0
  283. package/src/lib/subagent-contract.js +94 -0
  284. package/src/lib/todo-manager.js +185 -18
  285. package/src/lib/todo-persistence.js +417 -0
  286. package/src/lib/turn-binding-store.js +41 -23
  287. package/src/lib/with-file-lock.js +25 -1
  288. package/src/lib/ws-chat-handler.js +21 -0
  289. package/src/program-base.js +18 -0
  290. package/src/repl/advisor-command.js +103 -0
  291. package/src/repl/agent-repl.js +2332 -154
  292. package/src/repl/btw-command.js +93 -10
  293. package/src/repl/clipboard-image.js +119 -0
  294. package/src/repl/config-summary.js +64 -108
  295. package/src/repl/prompt-editor.js +233 -0
  296. package/src/repl/prompt-interactions.js +404 -0
  297. package/src/repl/prompt-stash.js +354 -0
  298. package/src/repl/prompt-suggestions.js +312 -0
  299. package/src/repl/repl-keybindings.js +227 -0
  300. package/src/repl/session-recap.js +217 -0
  301. package/src/repl/slash-command-registry.js +25 -4
  302. package/src/repl/terminal-layout.js +88 -0
  303. package/src/runtime/agent-core.js +1264 -325
  304. package/src/runtime/coding-agent-contract-shared.cjs +13 -1
  305. package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
  306. package/src/runtime/diagnostics.js +32 -13
  307. package/src/runtime/headless-runner.js +298 -118
  308. package/src/runtime/headless-stream.js +353 -95
  309. package/src/runtime/mcp-config.js +148 -14
  310. package/src/runtime/status-diagnostics-lite.js +137 -0
  311. package/src/assets/web-panel/assets/ChatBubbleRenderer-DDHDpBBA.js +0 -1
  312. package/src/assets/web-panel/assets/MarkdownRenderer-TabqHW9B.js +0 -1
  313. package/src/assets/web-panel/assets/RemoteSession-B0rkE09d.js +0 -5
  314. package/src/assets/web-panel/assets/WorkflowEditor-tzdyUk-1.js +0 -1
  315. package/src/assets/web-panel/assets/devWarning-DIrncrV1.js +0 -1
  316. package/src/assets/web-panel/assets/index-D-Swwqld.js +0 -1
  317. package/src/assets/web-panel/assets/index-FMZAMKy8.js +0 -1
@@ -58,6 +58,7 @@ export const WORKSPACE_TRANSACTION_ERROR = Object.freeze({
58
58
  RESOURCE_LIMIT: "WORKSPACE_TRANSACTION_RESOURCE_LIMIT",
59
59
  SNAPSHOT_RACE: "WORKSPACE_TRANSACTION_SNAPSHOT_RACE",
60
60
  LOCKED: "WORKSPACE_TRANSACTION_LOCKED",
61
+ WORKSPACE_LOCK_TIMEOUT: "WORKSPACE_LOCK_TIMEOUT",
61
62
  RECOVERY_REQUIRED: "WORKSPACE_TRANSACTION_RECOVERY_REQUIRED",
62
63
  LOCK_CORRUPT: "WORKSPACE_TRANSACTION_LOCK_CORRUPT",
63
64
  LOCK_OWNERSHIP_LOST: "WORKSPACE_TRANSACTION_LOCK_OWNERSHIP_LOST",
@@ -88,6 +89,19 @@ export const DEFAULT_WORKSPACE_TRANSACTION_LIMITS = Object.freeze({
88
89
  });
89
90
 
90
91
  const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,191}$/;
92
+ const CHECKPOINT_RESTORE_LOCK_PURPOSE = "checkpoint-restore";
93
+ const WORKSPACE_LOCK_OWNER_KEYS = Object.freeze([
94
+ "identityPolicy",
95
+ "pid",
96
+ "purpose",
97
+ "startedAt",
98
+ "token",
99
+ "transactionId",
100
+ "workspaceRoot",
101
+ ]);
102
+ const WORKSPACE_LOCK_RETENTION_REQUEST = Symbol(
103
+ "workspace-lock-retention-request",
104
+ );
91
105
  const TERMINAL_STATES = new Set([
92
106
  WORKSPACE_TRANSACTION_STATE.COMMITTED,
93
107
  WORKSPACE_TRANSACTION_STATE.ROLLED_BACK,
@@ -275,6 +289,23 @@ function assertNotFilesystemRoot(root) {
275
289
  }
276
290
  }
277
291
 
292
+ function assertCanonicalWorkspaceDirectory(value) {
293
+ const requested =
294
+ typeof value === "string" && value.trim() !== ""
295
+ ? path.resolve(value)
296
+ : value;
297
+ const canonical = assertSafeDirectory(value, "workspace root");
298
+ assertNotFilesystemRoot(canonical);
299
+ if (!samePath(requested, canonical)) {
300
+ throw codedError(
301
+ WORKSPACE_TRANSACTION_ERROR.PATH_ESCAPE,
302
+ "workspace root must be supplied as its canonical real path",
303
+ { workspaceRoot: requested, resolvedPath: canonical },
304
+ );
305
+ }
306
+ return canonical;
307
+ }
308
+
278
309
  function normalizeBoundedText(value, label, max = 256) {
279
310
  if (typeof value !== "string") {
280
311
  throw codedError(
@@ -1283,7 +1314,49 @@ function defaultLockDir() {
1283
1314
  );
1284
1315
  }
1285
1316
 
1286
- function assertOwnedPrivateDirectory(dir, label) {
1317
+ const DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS = 5_000;
1318
+ const DEFAULT_WORKSPACE_LOCK_RETRY_MS = 25;
1319
+ const MAX_WORKSPACE_LOCK_TIMEOUT_MS = 60_000;
1320
+ const MAX_WORKSPACE_LOCK_RETRY_MS = 1_000;
1321
+
1322
+ function boundedWorkspaceLockDelay(value, fallback, label, maximum) {
1323
+ const resolved = value === undefined ? fallback : Number(value);
1324
+ if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > maximum) {
1325
+ throw codedError(
1326
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
1327
+ `${label} must be a positive safe integer no greater than ${maximum}`,
1328
+ );
1329
+ }
1330
+ return resolved;
1331
+ }
1332
+
1333
+ function workspaceLockSleepSync(milliseconds) {
1334
+ try {
1335
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
1336
+ } catch {
1337
+ const deadline = Date.now() + milliseconds;
1338
+ while (Date.now() < deadline) {
1339
+ // SharedArrayBuffer is unavailable; retain a bounded synchronous wait.
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ function workspaceLockClockValue(now) {
1345
+ const value = Number(now());
1346
+ if (!Number.isFinite(value)) {
1347
+ throw codedError(
1348
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
1349
+ "workspace lock clock must return a finite number",
1350
+ );
1351
+ }
1352
+ return value;
1353
+ }
1354
+
1355
+ function assertOwnedPrivateDirectory(
1356
+ dir,
1357
+ label,
1358
+ { repairPermissions = true } = {},
1359
+ ) {
1287
1360
  const entry = fs.lstatSync(dir);
1288
1361
  if (entry.isSymbolicLink() || !entry.isDirectory()) {
1289
1362
  throw codedError(
@@ -1304,7 +1377,7 @@ function assertOwnedPrivateDirectory(dir, label) {
1304
1377
  );
1305
1378
  }
1306
1379
  if (process.platform !== "win32") {
1307
- fs.chmodSync(dir, 0o700);
1380
+ if (repairPermissions) fs.chmodSync(dir, 0o700);
1308
1381
  const secured = fs.lstatSync(dir);
1309
1382
  if ((Number(secured.mode) & 0o077) !== 0) {
1310
1383
  throw codedError(
@@ -1331,10 +1404,49 @@ function ownerIsValid(owner) {
1331
1404
  SAFE_ID.test(owner.transactionId) &&
1332
1405
  typeof owner.workspaceRoot === "string" &&
1333
1406
  path.isAbsolute(owner.workspaceRoot) &&
1407
+ (owner.purpose === undefined ||
1408
+ (typeof owner.purpose === "string" &&
1409
+ owner.purpose.length >= 1 &&
1410
+ owner.purpose.length <= 128 &&
1411
+ !owner.purpose.includes("\0"))) &&
1334
1412
  owner.identityPolicy === "pid-only-fail-closed"
1335
1413
  );
1336
1414
  }
1337
1415
 
1416
+ function hasExactWorkspaceLockOwnerShape(owner) {
1417
+ return (
1418
+ ownerIsValid(owner) &&
1419
+ Object.keys(owner).sort().join("\0") ===
1420
+ WORKSPACE_LOCK_OWNER_KEYS.join("\0")
1421
+ );
1422
+ }
1423
+
1424
+ function sameExactWorkspaceLockOwner(left, right) {
1425
+ return (
1426
+ hasExactWorkspaceLockOwnerShape(left) &&
1427
+ hasExactWorkspaceLockOwnerShape(right) &&
1428
+ canonicalWorkspaceEvidence(left) === canonicalWorkspaceEvidence(right)
1429
+ );
1430
+ }
1431
+
1432
+ function workspaceLockOwnerProjection(owner) {
1433
+ if (!ownerIsValid(owner)) {
1434
+ throw codedError(
1435
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
1436
+ "workspace lock owner cannot be projected safely",
1437
+ );
1438
+ }
1439
+ return Object.freeze({
1440
+ pid: owner.pid,
1441
+ startedAt: owner.startedAt,
1442
+ token: owner.token,
1443
+ transactionId: owner.transactionId,
1444
+ workspaceRoot: owner.workspaceRoot,
1445
+ purpose: owner.purpose,
1446
+ identityPolicy: owner.identityPolicy,
1447
+ });
1448
+ }
1449
+
1338
1450
  function readLockOwner(lockPath) {
1339
1451
  const ownerPath = path.join(lockPath, "owner.json");
1340
1452
  try {
@@ -1363,6 +1475,7 @@ function sameOwner(left, right) {
1363
1475
  left.token === right.token &&
1364
1476
  left.transactionId === right.transactionId &&
1365
1477
  samePath(left.workspaceRoot, right.workspaceRoot) &&
1478
+ left.purpose === right.purpose &&
1366
1479
  left.identityPolicy === right.identityPolicy
1367
1480
  );
1368
1481
  }
@@ -1387,10 +1500,14 @@ class WorkspaceLifetimeLock {
1387
1500
  isProcessAlive,
1388
1501
  now,
1389
1502
  ownerToken,
1503
+ purpose = "workspace-transaction",
1390
1504
  reclaimDead = false,
1391
1505
  expectedDeadOwner = null,
1506
+ requireExactExpectedDeadOwner = false,
1392
1507
  },
1393
1508
  ) {
1509
+ const normalizedTransactionId = normalizeId(transactionId);
1510
+ const normalizedPurpose = normalizeBoundedText(purpose, "purpose", 128);
1394
1511
  ensurePrivateDirectory(lockRoot);
1395
1512
  const lockPath = path.join(lockRoot, workspaceLockName(workspaceRoot));
1396
1513
  const ownerPath = path.join(lockPath, "owner.json");
@@ -1398,13 +1515,21 @@ class WorkspaceLifetimeLock {
1398
1515
  pid: process.pid,
1399
1516
  startedAt: now(),
1400
1517
  token: ownerToken(),
1401
- transactionId,
1518
+ transactionId: normalizedTransactionId,
1402
1519
  workspaceRoot,
1520
+ purpose: normalizedPurpose,
1403
1521
  // A live PID with an unavailable creation-time proof is never reclaimed.
1404
1522
  // This may require manual recovery after PID reuse, but cannot kill or
1405
1523
  // roll back underneath an unrelated process.
1406
1524
  identityPolicy: "pid-only-fail-closed",
1407
1525
  };
1526
+ if (!ownerIsValid(owner)) {
1527
+ throw codedError(
1528
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
1529
+ "workspace lock owner identity is invalid",
1530
+ { transactionId: normalizedTransactionId },
1531
+ );
1532
+ }
1408
1533
 
1409
1534
  for (let attempt = 0; attempt < 4; attempt += 1) {
1410
1535
  try {
@@ -1443,10 +1568,12 @@ class WorkspaceLifetimeLock {
1443
1568
  },
1444
1569
  );
1445
1570
  }
1446
- if (
1447
- !reclaimDead ||
1448
- (expectedDeadOwner && !sameOwner(expectedDeadOwner, incumbent))
1449
- ) {
1571
+ const expectedDeadOwnerMatches = expectedDeadOwner
1572
+ ? requireExactExpectedDeadOwner
1573
+ ? sameExactWorkspaceLockOwner(expectedDeadOwner, incumbent)
1574
+ : sameOwner(expectedDeadOwner, incumbent)
1575
+ : true;
1576
+ if (!reclaimDead || !expectedDeadOwnerMatches) {
1450
1577
  throw codedError(
1451
1578
  WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
1452
1579
  `workspace has a crashed transaction requiring recovery: ${workspaceRoot}`,
@@ -1457,7 +1584,9 @@ class WorkspaceLifetimeLock {
1457
1584
  },
1458
1585
  );
1459
1586
  }
1460
- WorkspaceLifetimeLock._reclaim(lockPath, incumbent, isProcessAlive);
1587
+ WorkspaceLifetimeLock._reclaim(lockPath, incumbent, isProcessAlive, {
1588
+ requireExactOwner: requireExactExpectedDeadOwner,
1589
+ });
1461
1590
  }
1462
1591
  }
1463
1592
  throw codedError(
@@ -1467,13 +1596,21 @@ class WorkspaceLifetimeLock {
1467
1596
  );
1468
1597
  }
1469
1598
 
1470
- static _reclaim(lockPath, observed, isProcessAlive) {
1599
+ static _reclaim(
1600
+ lockPath,
1601
+ observed,
1602
+ isProcessAlive,
1603
+ { requireExactOwner = false } = {},
1604
+ ) {
1471
1605
  const marker = path.join(lockPath, `.reclaim-${observed.token}`);
1472
1606
  atomicWrite(marker, `${canonicalWorkspaceEvidence(observed)}\n`, {
1473
1607
  maxBytes: 16 * 1024,
1474
1608
  });
1475
1609
  const current = readLockOwner(lockPath);
1476
- if (!sameOwner(current, observed) || isProcessAlive(observed.pid)) {
1610
+ const ownerMatches = requireExactOwner
1611
+ ? sameExactWorkspaceLockOwner(current, observed)
1612
+ : sameOwner(current, observed);
1613
+ if (!ownerMatches || isProcessAlive(observed.pid)) {
1477
1614
  try {
1478
1615
  fs.rmSync(marker, { force: true });
1479
1616
  } catch {
@@ -1536,8 +1673,12 @@ function assertNoOverlappingWorkspaceLock(
1536
1673
  lockDir,
1537
1674
  workspaceRoot,
1538
1675
  isProcessAlive,
1676
+ { expectedIgnoredOwner = null, requireExactIgnoredOwner = false } = {},
1539
1677
  ) {
1540
1678
  ensurePrivateDirectory(lockDir);
1679
+ const ignoredLockPath = expectedIgnoredOwner
1680
+ ? path.join(lockDir, workspaceLockName(expectedIgnoredOwner.workspaceRoot))
1681
+ : null;
1541
1682
  for (const name of fs.readdirSync(lockDir).sort()) {
1542
1683
  if (name === "coordination.lock") continue;
1543
1684
  const candidate = path.join(lockDir, name);
@@ -1562,6 +1703,24 @@ function assertNoOverlappingWorkspaceLock(
1562
1703
  { path: candidate },
1563
1704
  );
1564
1705
  }
1706
+ if (ignoredLockPath && samePath(candidate, ignoredLockPath)) {
1707
+ const ignoredOwnerMatches = requireExactIgnoredOwner
1708
+ ? sameExactWorkspaceLockOwner(owner, expectedIgnoredOwner)
1709
+ : sameOwner(owner, expectedIgnoredOwner);
1710
+ if (!ignoredOwnerMatches) {
1711
+ throw codedError(
1712
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
1713
+ "workspace recovery lock owner changed during registry validation",
1714
+ {
1715
+ workspaceRoot,
1716
+ lockPath: candidate,
1717
+ expectedTransactionId: expectedIgnoredOwner.transactionId,
1718
+ observedTransactionId: owner.transactionId,
1719
+ },
1720
+ );
1721
+ }
1722
+ continue;
1723
+ }
1565
1724
  if (!workspacesOverlap(owner.workspaceRoot, workspaceRoot)) continue;
1566
1725
  const alive = isProcessAlive(owner.pid);
1567
1726
  throw codedError(
@@ -1582,6 +1741,778 @@ function assertNoOverlappingWorkspaceLock(
1582
1741
  }
1583
1742
  }
1584
1743
 
1744
+ function preflightWorkspaceLockRoot(lockDir, workspaceRoot) {
1745
+ const requestedLockDir = path.resolve(lockDir);
1746
+ const projectedLockDir = canonicalPathThroughExistingAncestor(
1747
+ requestedLockDir,
1748
+ "lock directory",
1749
+ );
1750
+ if (pathsOverlap(projectedLockDir, workspaceRoot)) {
1751
+ throw codedError(
1752
+ WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
1753
+ "workspace transaction lock directory must be disjoint from the workspace",
1754
+ {
1755
+ workspaceRoot,
1756
+ lockDir: requestedLockDir,
1757
+ resolvedLockDir: projectedLockDir,
1758
+ },
1759
+ );
1760
+ }
1761
+ return { requestedLockDir, projectedLockDir };
1762
+ }
1763
+
1764
+ function ensureWorkspaceLockRoot(lockDir, workspaceRoot) {
1765
+ preflightWorkspaceLockRoot(lockDir, workspaceRoot);
1766
+ const canonicalLockDir = assertSafeDirectory(lockDir, "lock directory", {
1767
+ create: true,
1768
+ });
1769
+ if (pathsOverlap(canonicalLockDir, workspaceRoot)) {
1770
+ throw codedError(
1771
+ WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
1772
+ "workspace transaction lock directory must be disjoint from the workspace",
1773
+ { workspaceRoot, lockDir: canonicalLockDir },
1774
+ );
1775
+ }
1776
+ assertOwnedPrivateDirectory(canonicalLockDir, "lock directory");
1777
+ return canonicalLockDir;
1778
+ }
1779
+
1780
+ function inspectWorkspaceLockRoot(lockDir, workspaceRoot) {
1781
+ const { requestedLockDir } = preflightWorkspaceLockRoot(
1782
+ lockDir,
1783
+ workspaceRoot,
1784
+ );
1785
+ try {
1786
+ fs.lstatSync(requestedLockDir);
1787
+ } catch (cause) {
1788
+ if (cause?.code === "ENOENT") return null;
1789
+ throw codedError(
1790
+ WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
1791
+ `lock directory is unavailable: ${requestedLockDir}`,
1792
+ { path: requestedLockDir },
1793
+ cause,
1794
+ );
1795
+ }
1796
+ const canonicalLockDir = assertSafeDirectory(
1797
+ requestedLockDir,
1798
+ "lock directory",
1799
+ );
1800
+ if (pathsOverlap(canonicalLockDir, workspaceRoot)) {
1801
+ throw codedError(
1802
+ WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
1803
+ "workspace transaction lock directory must be disjoint from the workspace",
1804
+ { workspaceRoot, lockDir: canonicalLockDir },
1805
+ );
1806
+ }
1807
+ assertOwnedPrivateDirectory(canonicalLockDir, "lock directory", {
1808
+ repairPermissions: false,
1809
+ });
1810
+ return canonicalLockDir;
1811
+ }
1812
+
1813
+ function withWorkspaceRegistryCoordination(
1814
+ {
1815
+ lockDir,
1816
+ timeoutMs,
1817
+ retryMs = DEFAULT_WORKSPACE_LOCK_RETRY_MS,
1818
+ now = () => Date.now(),
1819
+ sleep = workspaceLockSleepSync,
1820
+ isProcessAlive = processAlive,
1821
+ ownerToken = () => randomUUID(),
1822
+ },
1823
+ callback,
1824
+ ) {
1825
+ return withFileLock(path.join(lockDir, "coordination"), callback, {
1826
+ failIfUnavailable: true,
1827
+ timeoutMs,
1828
+ retryMs: Math.min(retryMs, timeoutMs),
1829
+ maxRetryMs: Math.min(retryMs, timeoutMs),
1830
+ retryJitterMs: 0,
1831
+ _now: now,
1832
+ _sleep: sleep,
1833
+ _random: () => 0,
1834
+ _isProcessAlive: isProcessAlive,
1835
+ _ownerToken: ownerToken,
1836
+ });
1837
+ }
1838
+
1839
+ function acquireWorkspaceLifetimeLockOnce({
1840
+ lockDir,
1841
+ workspaceRoot,
1842
+ operationId,
1843
+ purpose,
1844
+ isProcessAlive,
1845
+ now,
1846
+ ownerToken,
1847
+ coordinationTimeoutMs,
1848
+ coordinationNow = () => Date.now(),
1849
+ coordinationSleep = workspaceLockSleepSync,
1850
+ coordinationIsProcessAlive = processAlive,
1851
+ coordinationOwnerToken = () => randomUUID(),
1852
+ }) {
1853
+ const canonicalLockDir = ensureWorkspaceLockRoot(lockDir, workspaceRoot);
1854
+ let lock = null;
1855
+ withWorkspaceRegistryCoordination(
1856
+ {
1857
+ lockDir: canonicalLockDir,
1858
+ timeoutMs: coordinationTimeoutMs,
1859
+ now: coordinationNow,
1860
+ sleep: coordinationSleep,
1861
+ isProcessAlive: coordinationIsProcessAlive,
1862
+ ownerToken: coordinationOwnerToken,
1863
+ },
1864
+ () => {
1865
+ assertNoOverlappingWorkspaceLock(
1866
+ canonicalLockDir,
1867
+ workspaceRoot,
1868
+ isProcessAlive,
1869
+ );
1870
+ lock = WorkspaceLifetimeLock.acquire(
1871
+ canonicalLockDir,
1872
+ workspaceRoot,
1873
+ operationId,
1874
+ {
1875
+ isProcessAlive,
1876
+ now,
1877
+ ownerToken,
1878
+ purpose,
1879
+ },
1880
+ );
1881
+ },
1882
+ );
1883
+ if (!lock) {
1884
+ throw codedError(
1885
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
1886
+ "workspace lock coordination completed without an ownership lease",
1887
+ { workspaceRoot, operationId },
1888
+ );
1889
+ }
1890
+ return lock;
1891
+ }
1892
+
1893
+ function acquireRecoveredWorkspaceLifetimeLock({
1894
+ lockDir,
1895
+ workspaceRoot,
1896
+ operationId,
1897
+ expectedDeadOwner,
1898
+ expectedPurpose = null,
1899
+ acquiredPurpose = "workspace-transaction",
1900
+ requireExactOwnerShape = false,
1901
+ isProcessAlive,
1902
+ now,
1903
+ ownerToken,
1904
+ coordinationTimeoutMs = DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
1905
+ coordinationRetryMs = DEFAULT_WORKSPACE_LOCK_RETRY_MS,
1906
+ coordinationNow = () => Date.now(),
1907
+ coordinationSleep = workspaceLockSleepSync,
1908
+ coordinationIsProcessAlive = processAlive,
1909
+ coordinationOwnerToken = () => randomUUID(),
1910
+ }) {
1911
+ const canonicalLockDir = ensureWorkspaceLockRoot(lockDir, workspaceRoot);
1912
+ if (
1913
+ !ownerIsValid(expectedDeadOwner) ||
1914
+ !samePath(expectedDeadOwner.workspaceRoot, workspaceRoot) ||
1915
+ (expectedPurpose !== null &&
1916
+ expectedDeadOwner.purpose !== expectedPurpose) ||
1917
+ (requireExactOwnerShape &&
1918
+ !hasExactWorkspaceLockOwnerShape(expectedDeadOwner))
1919
+ ) {
1920
+ throw codedError(
1921
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
1922
+ "workspace recovery requires an exact valid owner observation",
1923
+ { workspaceRoot, operationId },
1924
+ );
1925
+ }
1926
+ const lockPath = path.join(
1927
+ canonicalLockDir,
1928
+ workspaceLockName(workspaceRoot),
1929
+ );
1930
+ let lock = null;
1931
+ withWorkspaceRegistryCoordination(
1932
+ {
1933
+ lockDir: canonicalLockDir,
1934
+ timeoutMs: coordinationTimeoutMs,
1935
+ retryMs: coordinationRetryMs,
1936
+ now: coordinationNow,
1937
+ sleep: coordinationSleep,
1938
+ isProcessAlive: coordinationIsProcessAlive,
1939
+ ownerToken: coordinationOwnerToken,
1940
+ },
1941
+ () => {
1942
+ const currentOwner = readLockOwner(lockPath);
1943
+ const currentOwnerMatches = requireExactOwnerShape
1944
+ ? sameExactWorkspaceLockOwner(currentOwner, expectedDeadOwner)
1945
+ : sameOwner(currentOwner, expectedDeadOwner);
1946
+ if (!currentOwnerMatches) {
1947
+ throw codedError(
1948
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
1949
+ "workspace recovery owner changed before exact reclaim",
1950
+ {
1951
+ workspaceRoot,
1952
+ operationId,
1953
+ expectedTransactionId: expectedDeadOwner.transactionId,
1954
+ observedTransactionId: currentOwner?.transactionId || null,
1955
+ },
1956
+ );
1957
+ }
1958
+ if (isProcessAlive(currentOwner.pid)) {
1959
+ throw codedError(
1960
+ WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
1961
+ "workspace recovery owner became live before exact reclaim",
1962
+ {
1963
+ workspaceRoot,
1964
+ ownerPid: currentOwner.pid,
1965
+ ownerTransactionId: currentOwner.transactionId,
1966
+ },
1967
+ );
1968
+ }
1969
+ assertNoOverlappingWorkspaceLock(
1970
+ canonicalLockDir,
1971
+ workspaceRoot,
1972
+ isProcessAlive,
1973
+ {
1974
+ expectedIgnoredOwner: currentOwner,
1975
+ requireExactIgnoredOwner: requireExactOwnerShape,
1976
+ },
1977
+ );
1978
+ lock = WorkspaceLifetimeLock.acquire(
1979
+ canonicalLockDir,
1980
+ workspaceRoot,
1981
+ operationId,
1982
+ {
1983
+ isProcessAlive,
1984
+ now,
1985
+ ownerToken,
1986
+ purpose: acquiredPurpose,
1987
+ reclaimDead: true,
1988
+ expectedDeadOwner: currentOwner,
1989
+ requireExactExpectedDeadOwner: requireExactOwnerShape,
1990
+ },
1991
+ );
1992
+ },
1993
+ );
1994
+ if (!lock) {
1995
+ throw codedError(
1996
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
1997
+ "workspace recovery coordination completed without an ownership lease",
1998
+ { workspaceRoot, operationId },
1999
+ );
2000
+ }
2001
+ return lock;
2002
+ }
2003
+
2004
+ function publicWorkspaceLockRuntime(options) {
2005
+ const canonicalLockDir = path.resolve(defaultLockDir());
2006
+ const requestedLockDir = path.resolve(options.lockDir || canonicalLockDir);
2007
+ const permitsTestInjection =
2008
+ process.env.NODE_ENV === "test" &&
2009
+ options.allowNonCanonicalLockDirForTests === true;
2010
+ const hasRuntimeInjection = [
2011
+ "_now",
2012
+ "_sleep",
2013
+ "_isProcessAlive",
2014
+ "_ownerToken",
2015
+ ].some((key) => Object.hasOwn(options, key));
2016
+ if (
2017
+ (!samePath(requestedLockDir, canonicalLockDir) || hasRuntimeInjection) &&
2018
+ !permitsTestInjection
2019
+ ) {
2020
+ throw codedError(
2021
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2022
+ "workspace lock authority and runtime hooks are test-only",
2023
+ { requestedLockDir, canonicalLockDir },
2024
+ );
2025
+ }
2026
+ const now = options._now || (() => Date.now());
2027
+ const sleep = options._sleep || workspaceLockSleepSync;
2028
+ const isProcessAlive = options._isProcessAlive || processAlive;
2029
+ const ownerToken = options._ownerToken || (() => randomUUID());
2030
+ for (const [label, value] of [
2031
+ ["_now", now],
2032
+ ["_sleep", sleep],
2033
+ ["_isProcessAlive", isProcessAlive],
2034
+ ["_ownerToken", ownerToken],
2035
+ ]) {
2036
+ if (typeof value !== "function") {
2037
+ throw codedError(
2038
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2039
+ `${label} must be a function`,
2040
+ );
2041
+ }
2042
+ }
2043
+ return {
2044
+ lockDir: requestedLockDir,
2045
+ now,
2046
+ sleep,
2047
+ isProcessAlive,
2048
+ ownerToken,
2049
+ };
2050
+ }
2051
+
2052
+ function isLiveWorkspaceLockConflict(error) {
2053
+ return (
2054
+ error?.code === WORKSPACE_TRANSACTION_ERROR.OVERLAPPING_WORKSPACE ||
2055
+ error?.code === WORKSPACE_TRANSACTION_ERROR.LOCKED ||
2056
+ error?.code === "STATE_LOCK_UNAVAILABLE"
2057
+ );
2058
+ }
2059
+
2060
+ function asyncWorkspaceLockCallbackError(cause = null) {
2061
+ return codedError(
2062
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2063
+ "workspace lock callback must complete synchronously and cannot return a thenable",
2064
+ {},
2065
+ cause,
2066
+ );
2067
+ }
2068
+
2069
+ function isAsyncFunction(callback) {
2070
+ return Object.prototype.toString.call(callback) === "[object AsyncFunction]";
2071
+ }
2072
+
2073
+ function assertSynchronousWorkspaceLockCallback(callback) {
2074
+ if (typeof callback !== "function") {
2075
+ throw codedError(
2076
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2077
+ "workspace lock callback must be a function",
2078
+ );
2079
+ }
2080
+ if (isAsyncFunction(callback)) throw asyncWorkspaceLockCallbackError();
2081
+ }
2082
+
2083
+ function assertNonThenableWorkspaceLockResult(result) {
2084
+ if (
2085
+ result === null ||
2086
+ (typeof result !== "object" && typeof result !== "function")
2087
+ ) {
2088
+ return;
2089
+ }
2090
+ let then;
2091
+ try {
2092
+ then = result.then;
2093
+ } catch (cause) {
2094
+ throw asyncWorkspaceLockCallbackError(cause);
2095
+ }
2096
+ if (typeof then === "function") throw asyncWorkspaceLockCallbackError();
2097
+ }
2098
+
2099
+ function runWorkspaceLockCallbackSync({
2100
+ lock,
2101
+ workspaceRoot,
2102
+ operationId,
2103
+ callback,
2104
+ priorOwner = null,
2105
+ recoveryOfOperationId = operationId,
2106
+ }) {
2107
+ const publicOwner = workspaceLockOwnerProjection(lock.owner);
2108
+ const publicPriorOwner = priorOwner
2109
+ ? workspaceLockOwnerProjection(priorOwner)
2110
+ : null;
2111
+ let retentionRequest = null;
2112
+ const lease = Object.freeze({
2113
+ canonicalWorkspaceRoot: workspaceRoot,
2114
+ owner: publicOwner,
2115
+ priorOwner: publicPriorOwner,
2116
+ recoveryOfOperationId,
2117
+ assertOwned: () => lock.assertOwned(),
2118
+ retainForRecovery: (reason) => {
2119
+ lock.assertOwned();
2120
+ if (lock.owner.purpose !== CHECKPOINT_RESTORE_LOCK_PURPOSE) {
2121
+ throw codedError(
2122
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2123
+ "only checkpoint-restore locks can be retained for recovery",
2124
+ { operationId, purpose: lock.owner.purpose },
2125
+ );
2126
+ }
2127
+ if (retentionRequest) {
2128
+ throw codedError(
2129
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2130
+ "workspace lock recovery retention was already requested",
2131
+ { operationId },
2132
+ );
2133
+ }
2134
+ const normalizedReason = normalizeBoundedText(
2135
+ reason,
2136
+ "workspace lock recovery reason",
2137
+ 512,
2138
+ );
2139
+ const request = {
2140
+ error: null,
2141
+ reason: normalizedReason,
2142
+ };
2143
+ const error = codedError(
2144
+ WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
2145
+ "workspace lock retention requested for durable recovery",
2146
+ {
2147
+ workspaceRoot,
2148
+ operationId,
2149
+ retentionReason: normalizedReason,
2150
+ },
2151
+ );
2152
+ request.error = error;
2153
+ Object.defineProperty(error, WORKSPACE_LOCK_RETENTION_REQUEST, {
2154
+ value: request,
2155
+ });
2156
+ retentionRequest = request;
2157
+ throw error;
2158
+ },
2159
+ });
2160
+ let result;
2161
+ let callbackError;
2162
+ let callbackThrew = false;
2163
+ try {
2164
+ result = callback(lease);
2165
+ assertNonThenableWorkspaceLockResult(result);
2166
+ } catch (error) {
2167
+ callbackThrew = true;
2168
+ callbackError = error;
2169
+ }
2170
+
2171
+ const retentionQualified = !!(
2172
+ callbackThrew &&
2173
+ retentionRequest &&
2174
+ callbackError === retentionRequest.error &&
2175
+ callbackError?.code === WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED &&
2176
+ callbackError?.[WORKSPACE_LOCK_RETENTION_REQUEST] === retentionRequest
2177
+ );
2178
+ let releaseError = null;
2179
+ if (retentionQualified) {
2180
+ try {
2181
+ lock.assertOwned();
2182
+ } catch (error) {
2183
+ releaseError = error;
2184
+ }
2185
+ if (!releaseError) {
2186
+ const retainedOwner = workspaceLockOwnerProjection(lock.owner);
2187
+ throw codedError(
2188
+ WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
2189
+ "workspace lock was retained for durable recovery",
2190
+ {
2191
+ workspaceRoot,
2192
+ operationId,
2193
+ retentionReason: retentionRequest.reason,
2194
+ workspaceLockRetained: true,
2195
+ retainedOwner,
2196
+ priorOwner: publicPriorOwner,
2197
+ newOwner: retainedOwner,
2198
+ recoveryOfOperationId,
2199
+ },
2200
+ callbackError,
2201
+ );
2202
+ }
2203
+ }
2204
+ if (!releaseError) {
2205
+ try {
2206
+ lock.release();
2207
+ } catch (error) {
2208
+ releaseError = error;
2209
+ }
2210
+ }
2211
+ if (releaseError) {
2212
+ if (callbackThrew) releaseError.callbackError = callbackError;
2213
+ throw releaseError;
2214
+ }
2215
+ if (callbackThrew) throw callbackError;
2216
+ return result;
2217
+ }
2218
+
2219
+ function normalizeCheckpointRestorePurpose(value) {
2220
+ const purpose = normalizeBoundedText(
2221
+ value ?? CHECKPOINT_RESTORE_LOCK_PURPOSE,
2222
+ "purpose",
2223
+ 128,
2224
+ );
2225
+ if (purpose !== CHECKPOINT_RESTORE_LOCK_PURPOSE) {
2226
+ throw codedError(
2227
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2228
+ `workspace recovery only accepts purpose ${CHECKPOINT_RESTORE_LOCK_PURPOSE}`,
2229
+ { purpose },
2230
+ );
2231
+ }
2232
+ return purpose;
2233
+ }
2234
+
2235
+ function normalizeExpectedCheckpointRestoreOwner(
2236
+ value,
2237
+ workspaceRoot,
2238
+ operationId,
2239
+ ) {
2240
+ if (
2241
+ !hasExactWorkspaceLockOwnerShape(value) ||
2242
+ value.purpose !== CHECKPOINT_RESTORE_LOCK_PURPOSE ||
2243
+ value.transactionId !== operationId ||
2244
+ !samePath(value.workspaceRoot, workspaceRoot)
2245
+ ) {
2246
+ throw codedError(
2247
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2248
+ "expectedOwner must be the complete exact checkpoint-restore owner for this workspace and operation",
2249
+ { workspaceRoot, operationId },
2250
+ );
2251
+ }
2252
+ return workspaceLockOwnerProjection(value);
2253
+ }
2254
+
2255
+ /**
2256
+ * Hold the canonical cross-process workspace lock for one synchronous
2257
+ * operation. The coordination lock is held only while inspecting and claiming
2258
+ * the registry; waits for a live overlapping owner happen outside it.
2259
+ */
2260
+ export function withWorkspaceLockSync(options = {}, callback) {
2261
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
2262
+ throw codedError(
2263
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2264
+ "workspace lock options must be an object",
2265
+ );
2266
+ }
2267
+ assertSynchronousWorkspaceLockCallback(callback);
2268
+
2269
+ const workspaceRoot = assertCanonicalWorkspaceDirectory(
2270
+ options.workspaceRoot,
2271
+ );
2272
+ const operationId = normalizeId(options.operationId, "operationId");
2273
+ const purpose = normalizeBoundedText(
2274
+ options.purpose ?? "checkpoint-operation",
2275
+ "purpose",
2276
+ 128,
2277
+ );
2278
+ const timeoutMs = boundedWorkspaceLockDelay(
2279
+ options.timeoutMs,
2280
+ DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
2281
+ "timeoutMs",
2282
+ MAX_WORKSPACE_LOCK_TIMEOUT_MS,
2283
+ );
2284
+ const retryMs = boundedWorkspaceLockDelay(
2285
+ options.retryMs,
2286
+ DEFAULT_WORKSPACE_LOCK_RETRY_MS,
2287
+ "retryMs",
2288
+ MAX_WORKSPACE_LOCK_RETRY_MS,
2289
+ );
2290
+ const runtime = publicWorkspaceLockRuntime(options);
2291
+ const startedAt = workspaceLockClockValue(runtime.now);
2292
+ const deadline = startedAt + timeoutMs;
2293
+ const maxAttempts = Math.ceil(timeoutMs / retryMs) + 1;
2294
+ let attempts = 0;
2295
+ let lock = null;
2296
+
2297
+ while (!lock) {
2298
+ attempts += 1;
2299
+ const currentTime = workspaceLockClockValue(runtime.now);
2300
+ const remaining = Math.max(1, Math.ceil(deadline - currentTime));
2301
+ try {
2302
+ lock = acquireWorkspaceLifetimeLockOnce({
2303
+ lockDir: runtime.lockDir,
2304
+ workspaceRoot,
2305
+ operationId,
2306
+ purpose,
2307
+ isProcessAlive: runtime.isProcessAlive,
2308
+ now: runtime.now,
2309
+ ownerToken: runtime.ownerToken,
2310
+ coordinationTimeoutMs: Math.max(1, Math.min(retryMs, remaining)),
2311
+ coordinationNow: runtime.now,
2312
+ coordinationSleep: runtime.sleep,
2313
+ coordinationIsProcessAlive: runtime.isProcessAlive,
2314
+ coordinationOwnerToken: runtime.ownerToken,
2315
+ });
2316
+ } catch (error) {
2317
+ if (
2318
+ error?.code === "STATE_LOCK_UNAVAILABLE" &&
2319
+ error?.cause?.code === "STATE_LOCK_OWNER_CORRUPT"
2320
+ ) {
2321
+ throw codedError(
2322
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
2323
+ "workspace lock coordination ownership is corrupt",
2324
+ { workspaceRoot, operationId },
2325
+ error,
2326
+ );
2327
+ }
2328
+ if (!isLiveWorkspaceLockConflict(error)) throw error;
2329
+ const afterAttempt = workspaceLockClockValue(runtime.now);
2330
+ if (afterAttempt >= deadline || attempts >= maxAttempts) {
2331
+ throw codedError(
2332
+ WORKSPACE_TRANSACTION_ERROR.WORKSPACE_LOCK_TIMEOUT,
2333
+ `timed out waiting for the workspace lock: ${workspaceRoot}`,
2334
+ {
2335
+ workspaceRoot,
2336
+ operationId,
2337
+ purpose,
2338
+ timeoutMs,
2339
+ attempts,
2340
+ ownerWorkspaceRoot: error.ownerWorkspaceRoot || workspaceRoot,
2341
+ ownerOperationId:
2342
+ error.ownerTransactionId || error.transactionId || null,
2343
+ ownerPid: error.ownerPid || null,
2344
+ },
2345
+ error,
2346
+ );
2347
+ }
2348
+ runtime.sleep(
2349
+ Math.max(1, Math.min(retryMs, Math.ceil(deadline - afterAttempt))),
2350
+ );
2351
+ }
2352
+ }
2353
+
2354
+ return runWorkspaceLockCallbackSync({
2355
+ lock,
2356
+ workspaceRoot,
2357
+ operationId,
2358
+ callback,
2359
+ });
2360
+ }
2361
+
2362
+ /**
2363
+ * Observe the exact checkpoint-restore owner for a known workspace and saga
2364
+ * operation. This never scans for another owner and never creates or reclaims
2365
+ * lock state; a later recovery takeover must still perform its own exact CAS.
2366
+ */
2367
+ export function inspectWorkspaceLockOwnerSync(options = {}) {
2368
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
2369
+ throw codedError(
2370
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2371
+ "workspace lock inspection options must be an object",
2372
+ );
2373
+ }
2374
+ const workspaceRoot = assertCanonicalWorkspaceDirectory(
2375
+ options.workspaceRoot,
2376
+ );
2377
+ const operationId = normalizeId(options.operationId, "operationId");
2378
+ const purpose = normalizeCheckpointRestorePurpose(options.purpose);
2379
+ const runtime = publicWorkspaceLockRuntime(options);
2380
+ const lockDir = inspectWorkspaceLockRoot(runtime.lockDir, workspaceRoot);
2381
+ if (!lockDir) return null;
2382
+ const lockPath = path.join(lockDir, workspaceLockName(workspaceRoot));
2383
+ let entry;
2384
+ try {
2385
+ entry = fs.lstatSync(lockPath);
2386
+ } catch (cause) {
2387
+ if (cause?.code === "ENOENT") return null;
2388
+ throw codedError(
2389
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
2390
+ "workspace lock cannot be inspected safely",
2391
+ { workspaceRoot, operationId },
2392
+ cause,
2393
+ );
2394
+ }
2395
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
2396
+ throw codedError(
2397
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
2398
+ "workspace lock must be a real directory",
2399
+ { workspaceRoot, operationId, lockPath },
2400
+ );
2401
+ }
2402
+ assertOwnedPrivateDirectory(lockPath, "workspace lock", {
2403
+ repairPermissions: false,
2404
+ });
2405
+ const owner = readLockOwner(lockPath);
2406
+ if (
2407
+ !hasExactWorkspaceLockOwnerShape(owner) ||
2408
+ !samePath(owner.workspaceRoot, workspaceRoot)
2409
+ ) {
2410
+ throw codedError(
2411
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
2412
+ "workspace lock has corrupt or non-canonical ownership",
2413
+ { workspaceRoot, operationId },
2414
+ );
2415
+ }
2416
+ if (owner.transactionId !== operationId || owner.purpose !== purpose) {
2417
+ throw codedError(
2418
+ WORKSPACE_TRANSACTION_ERROR.RECOVERY_REQUIRED,
2419
+ "workspace is owned by a different durable operation",
2420
+ {
2421
+ workspaceRoot,
2422
+ operationId,
2423
+ purpose,
2424
+ ownerOperationId: owner.transactionId,
2425
+ ownerPurpose: owner.purpose,
2426
+ },
2427
+ );
2428
+ }
2429
+ return workspaceLockOwnerProjection(owner);
2430
+ }
2431
+
2432
+ /**
2433
+ * Replace one exact dead checkpoint-restore owner and hold the replacement for
2434
+ * a synchronous recovery attempt. The saga operation id is deliberately
2435
+ * stable across takeover; an independent recovery-attempt id belongs in the
2436
+ * durable saga journal, not in the workspace lock identity.
2437
+ */
2438
+ export function withWorkspaceRecoveryLockSync(options = {}, callback) {
2439
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
2440
+ throw codedError(
2441
+ WORKSPACE_TRANSACTION_ERROR.INVALID_ARGUMENT,
2442
+ "workspace recovery lock options must be an object",
2443
+ );
2444
+ }
2445
+ assertSynchronousWorkspaceLockCallback(callback);
2446
+ const workspaceRoot = assertCanonicalWorkspaceDirectory(
2447
+ options.workspaceRoot,
2448
+ );
2449
+ const operationId = normalizeId(options.operationId, "operationId");
2450
+ const purpose = normalizeCheckpointRestorePurpose(options.purpose);
2451
+ const expectedOwner = normalizeExpectedCheckpointRestoreOwner(
2452
+ options.expectedOwner,
2453
+ workspaceRoot,
2454
+ operationId,
2455
+ );
2456
+ const timeoutMs = boundedWorkspaceLockDelay(
2457
+ options.timeoutMs,
2458
+ DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
2459
+ "timeoutMs",
2460
+ MAX_WORKSPACE_LOCK_TIMEOUT_MS,
2461
+ );
2462
+ const retryMs = boundedWorkspaceLockDelay(
2463
+ options.retryMs,
2464
+ DEFAULT_WORKSPACE_LOCK_RETRY_MS,
2465
+ "retryMs",
2466
+ MAX_WORKSPACE_LOCK_RETRY_MS,
2467
+ );
2468
+ const runtime = publicWorkspaceLockRuntime(options);
2469
+ let lock;
2470
+ try {
2471
+ lock = acquireRecoveredWorkspaceLifetimeLock({
2472
+ lockDir: runtime.lockDir,
2473
+ workspaceRoot,
2474
+ operationId,
2475
+ expectedDeadOwner: expectedOwner,
2476
+ expectedPurpose: purpose,
2477
+ acquiredPurpose: purpose,
2478
+ requireExactOwnerShape: true,
2479
+ isProcessAlive: runtime.isProcessAlive,
2480
+ now: runtime.now,
2481
+ ownerToken: runtime.ownerToken,
2482
+ coordinationTimeoutMs: timeoutMs,
2483
+ coordinationRetryMs: retryMs,
2484
+ coordinationNow: runtime.now,
2485
+ coordinationSleep: runtime.sleep,
2486
+ coordinationIsProcessAlive: runtime.isProcessAlive,
2487
+ coordinationOwnerToken: runtime.ownerToken,
2488
+ });
2489
+ } catch (error) {
2490
+ if (error?.code !== "STATE_LOCK_UNAVAILABLE") throw error;
2491
+ if (error?.cause?.code === "STATE_LOCK_OWNER_CORRUPT") {
2492
+ throw codedError(
2493
+ WORKSPACE_TRANSACTION_ERROR.LOCK_CORRUPT,
2494
+ "workspace recovery coordination ownership is corrupt",
2495
+ { workspaceRoot, operationId },
2496
+ error,
2497
+ );
2498
+ }
2499
+ throw codedError(
2500
+ WORKSPACE_TRANSACTION_ERROR.WORKSPACE_LOCK_TIMEOUT,
2501
+ `timed out waiting for workspace recovery coordination: ${workspaceRoot}`,
2502
+ { workspaceRoot, operationId, purpose, timeoutMs },
2503
+ error,
2504
+ );
2505
+ }
2506
+ return runWorkspaceLockCallbackSync({
2507
+ lock,
2508
+ workspaceRoot,
2509
+ operationId,
2510
+ callback,
2511
+ priorOwner: expectedOwner,
2512
+ recoveryOfOperationId: operationId,
2513
+ });
2514
+ }
2515
+
1585
2516
  function safeCurrentEntry(root, relative, expectedRootIdentity) {
1586
2517
  assertWorkspaceRootIdentity(root, expectedRootIdentity);
1587
2518
  const native = nativePath(relative);
@@ -2779,69 +3710,44 @@ export class WorkspaceTransactionManager {
2779
3710
  }
2780
3711
 
2781
3712
  _preflightLockRoot(workspaceRoot = null) {
2782
- const requestedLockDir = path.resolve(this.lockDir);
2783
- const projectedLockDir = canonicalPathThroughExistingAncestor(
2784
- requestedLockDir,
2785
- "lock directory",
2786
- );
2787
- if (workspaceRoot && pathsOverlap(projectedLockDir, workspaceRoot)) {
2788
- throw codedError(
2789
- WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
2790
- "workspace transaction lock directory must be disjoint from the workspace",
2791
- {
2792
- workspaceRoot,
2793
- lockDir: requestedLockDir,
2794
- resolvedLockDir: projectedLockDir,
2795
- },
2796
- );
3713
+ if (!workspaceRoot) {
3714
+ const requestedLockDir = path.resolve(this.lockDir);
3715
+ return {
3716
+ requestedLockDir,
3717
+ projectedLockDir: canonicalPathThroughExistingAncestor(
3718
+ requestedLockDir,
3719
+ "lock directory",
3720
+ ),
3721
+ };
2797
3722
  }
2798
- return { requestedLockDir, projectedLockDir };
3723
+ return preflightWorkspaceLockRoot(this.lockDir, workspaceRoot);
2799
3724
  }
2800
3725
 
2801
3726
  _ensureLockRoot(workspaceRoot = null) {
2802
- this._preflightLockRoot(workspaceRoot);
3727
+ if (workspaceRoot) {
3728
+ this.lockDir = ensureWorkspaceLockRoot(this.lockDir, workspaceRoot);
3729
+ return this.lockDir;
3730
+ }
3731
+ this._preflightLockRoot();
2803
3732
  this.lockDir = assertSafeDirectory(this.lockDir, "lock directory", {
2804
3733
  create: true,
2805
3734
  });
2806
- if (workspaceRoot && pathsOverlap(this.lockDir, workspaceRoot)) {
2807
- throw codedError(
2808
- WORKSPACE_TRANSACTION_ERROR.INVALID_PATH,
2809
- "workspace transaction lock directory must be disjoint from the workspace",
2810
- { workspaceRoot, lockDir: this.lockDir },
2811
- );
2812
- }
2813
3735
  assertOwnedPrivateDirectory(this.lockDir, "lock directory");
2814
3736
  return this.lockDir;
2815
3737
  }
2816
3738
 
2817
3739
  _acquireWorkspaceLock(workspaceRoot, transactionId) {
2818
- this._ensureLockRoot(workspaceRoot);
2819
- let lock = null;
2820
- withFileLock(
2821
- path.join(this.lockDir, "coordination"),
2822
- () => {
2823
- assertNoOverlappingWorkspaceLock(
2824
- this.lockDir,
2825
- workspaceRoot,
2826
- this._isProcessAlive,
2827
- );
2828
- lock = WorkspaceLifetimeLock.acquire(
2829
- this.lockDir,
2830
- workspaceRoot,
2831
- transactionId,
2832
- {
2833
- isProcessAlive: this._isProcessAlive,
2834
- now: this._now,
2835
- ownerToken: this._ownerToken,
2836
- },
2837
- );
2838
- },
2839
- {
2840
- failIfUnavailable: true,
2841
- timeoutMs: 5_000,
2842
- },
2843
- );
2844
- return lock;
3740
+ this.lockDir = this._ensureLockRoot(workspaceRoot);
3741
+ return acquireWorkspaceLifetimeLockOnce({
3742
+ lockDir: this.lockDir,
3743
+ workspaceRoot,
3744
+ operationId: transactionId,
3745
+ purpose: "workspace-transaction",
3746
+ isProcessAlive: this._isProcessAlive,
3747
+ now: this._now,
3748
+ ownerToken: this._ownerToken,
3749
+ coordinationTimeoutMs: DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
3750
+ });
2845
3751
  }
2846
3752
 
2847
3753
  _preflightStateRoot(workspaceRoot = null) {
@@ -3484,18 +4390,15 @@ export class WorkspaceTransactionManager {
3484
4390
  }
3485
4391
  let lock;
3486
4392
  try {
3487
- lock = WorkspaceLifetimeLock.acquire(
3488
- this.lockDir,
3489
- state.workspaceRoot,
3490
- state.id,
3491
- {
3492
- isProcessAlive: this._isProcessAlive,
3493
- now: this._now,
3494
- ownerToken: this._ownerToken,
3495
- reclaimDead: true,
3496
- expectedDeadOwner: deadOwner,
3497
- },
3498
- );
4393
+ lock = acquireRecoveredWorkspaceLifetimeLock({
4394
+ lockDir: this.lockDir,
4395
+ workspaceRoot: state.workspaceRoot,
4396
+ operationId: state.id,
4397
+ expectedDeadOwner: deadOwner,
4398
+ isProcessAlive: this._isProcessAlive,
4399
+ now: this._now,
4400
+ ownerToken: this._ownerToken,
4401
+ });
3499
4402
  state.owner = deepClone(lock.owner);
3500
4403
  state.updatedAt = new Date(this._now()).toISOString();
3501
4404
  state.stateDigest = stateDigest(state);