chainlesschain 0.162.198 → 0.163.0

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 (197) hide show
  1. package/bin/chainlesschain.js +14 -6
  2. package/package.json +5 -1
  3. package/scripts/command-lifecycle-report.mjs +94 -0
  4. package/scripts/postinstall.mjs +19 -3
  5. package/scripts/repair-node-pty.mjs +74 -0
  6. package/src/assets/web-panel/assets/{AIOps-CSN-D2xb.js → AIOps-CSy9BB6Z.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-CBSMlGzv.js → ActionButton-CFyrEVfq.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-3yr3Nt2t.js → Analytics-8419vb47.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-1b-o3lMH.js → AppLayout-CHu3Hkdx.js} +5 -5
  10. package/src/assets/web-panel/assets/{Artifacts-Dr8rJi17.js → Artifacts-DJnt57Fs.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-2V689HC2.js → Audit-CHMq9ajE.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-DIxkNrD_.js → BackgroundAgents-Cyfa4ub2.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-Pg6BcwrE.js → Backup-ACXUrUv7.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-CLUpqyfH.js → BaseInput-DjC-3qRE.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-qVS8rD4G.js → Chat-7_VDQ851.js} +6 -6
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-ti49X12Q.js → ChatBubbleRenderer-B4zct0dY.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-Bs9AaEay.js → Checkbox-DxbDl_rk.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-Dvc1mq8q.js → Codegen-BvZa5854.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-C4_biO7A.js → Col-CX5Ow2Zl.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-al25XQUV.js → Community-wFun-jSg.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-D-ZDB8M1.js → Compact-C6vJDuSn.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-6lIfDz1W.js → Compliance-Cm4sBytH.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-DdI48XMJ.js → Cowork-DiEcMwE8.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-DF5HZd2g.js → Cron-RbeexsSQ.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-DuxeSvt3.js → Crosschain-BBsyCKOp.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-B9yHkUTd.js → DID-DE36g7Hz.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-Z5EuI3IM.js → Dashboard-CdfLSTz1.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-vuRfO3dY.js → Dropdown-CyaT6SyW.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-QFLCc1uL.js → EmailListRenderer-Nv_66bkM.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZ7EXkFJ.js → FamilyGuardDashboard-Di-Sb-Ad.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-DycIEg2s.js → Federation-B3mdht-U.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-Ba0H_7yG.js → FormItemContext-BeP93u4f.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer--JgSTK-C.js → GenericCardRenderer-CETLayUr.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-DEvTbldE.js → Git-Bsat6hgI.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BJIljy18.js → Governance-YVmA7SwN.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-CVyW3G3q.js → Inference-DCa94nGl.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-v8-borge.js → KnowledgeGraph-CIKEU7T0.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-BCdS7gGo.js → Logs-DUxP0MnS.js} +2 -2
  39. package/src/assets/web-panel/assets/{MarkdownRenderer-CU-JB2Ns.js → MarkdownRenderer-Mw5o3e7M.js} +1 -1
  40. package/src/assets/web-panel/assets/{Marketplace-AN6tGA2B.js → Marketplace-BU0X2lqu.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-DGViXASN.js → McpTools-BSsg58nZ.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-Bpvmx9X4.js → Memory-CjqNIXlO.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-CTP-DOTC.js → MobileBridge-DSsa4doG.js} +1 -1
  44. package/src/assets/web-panel/assets/MobileProjects-DLl1qzid.js +1 -0
  45. package/src/assets/web-panel/assets/{Mtc-ndYr0M1s.js → Mtc-Du080G7t.js} +5 -5
  46. package/src/assets/web-panel/assets/{MtcAudit-Ci0-YK7W.js → MtcAudit-JzQk-arw.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-BehSTFW7.js → Multisig-BMVT5Uln.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-B91_YBLv.js → NLProgramming-D_38QYjs.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-BQlJ0-gq.js → Notes-DvbdjNm5.js} +3 -3
  50. package/src/assets/web-panel/assets/{NotificationSettings-CDgYMjYK.js → NotificationSettings-DVHCW6ii.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer-BJkSlKeH.js → OrderTableRenderer-DmMBFD3g.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-B-b3cWCp.js → Organization-DC-9pAmP.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-CILUL4nL.js → Overflow-BRxWq0Qq.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-BTCFBkF0.js → P2P-CUdzhyLH.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-BAR6AUx5.js → PdhVaultBrowser-CwG21s9P.js} +3 -3
  56. package/src/assets/web-panel/assets/{Permissions-RRf6HbWJ.js → Permissions-6bHd4zgK.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-BGTMiICa.js → PersonalDataHub-BXgTDCba.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-945qY9YF.js → Pipeline-BFhKCBNj.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-DPZwI6lJ.js → Privacy-Byz9IqUF.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-BPH2NRKc.js → ProjectInit-B9LNoyoN.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-BZCRNCvb.js → ProjectSettings-DEj4eVdl.js} +2 -2
  62. package/src/assets/web-panel/assets/{Projects-BTI__tBt.js → Projects-BAAvus06.js} +1 -1
  63. package/src/assets/web-panel/assets/Providers-Bu_pQsk6.js +1 -0
  64. package/src/assets/web-panel/assets/{QrScannerModal-N6rbqt1r.js → QrScannerModal-CeDey0SJ.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-DrwjPS7M.js → QuickAsk-exUvRIgX.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-Cog9Wd99.js → Recommend-4hJWmtSI.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-DC9pynjM.js → RemoteSession-BmYUpl0m.js} +2 -2
  68. package/src/assets/web-panel/assets/{Reputation-D4th4gKS.js → Reputation-B_txYAyY.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-ogiWR6v9.js → Row-BPtqOqGh.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-uYlk-2Oj.js → RssFeed-CyWTxmq0.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-DYzzwvtM.js → Search-cItU1tmC.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-Da7YOyX6.js → Security-B0Z9_BUt.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-CLI1VM1X.js → Services-Dzpz2uFp.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-DK77dzDC.js → Skeleton-CBtoylP8.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-C_z7Lmhl.js → Skills-Cw9RJiVn.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-D6413Q8L.js → Sla-D2vLfvgl.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-C07ydbj2.js → SpeechSettings-I9dKbcOU.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-DGOm2W22.js → SyncSettings-BOju53RT.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-Cr_PVyLM.js → Tasks-C3LBy_7x.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-BhL2mZ0t.js → Templates-C2ttpPql.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-CPk5oyUr.js → Tenant-XQ6Qg8-Q.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-ClhqUsqV.js → Terminal-DYB9dUnS.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-BCC7MV5a.js → TimelineRenderer-gShtO4I8.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-DD1Jt4oO.js → Tokens-BvoJTCJg.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-BCydFvN2.js → Trigger-BwYRTMY_.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-DDHT1noa.js → Trust-C5rt6fCF.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-CWPZC7oI.js → UkeySign-DPBrHUgL.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-9Ea0nJR0.js → VideoEditing-BcxywO_K.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-BjxhLxTf.js → Wallet-CZvTpsPz.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-C6uCye67.js → WebAuthn-LI_jAuOx.js} +4 -4
  91. package/src/assets/web-panel/assets/{WorkflowEditor-d2g4reYo.js → WorkflowEditor-B2a6bANB.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-frTc6ScX.js → chat-B6DPMfRB.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-B5K6lG3v.js → colors-IpLhw9T8.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-Cdn46r8D.js → compact-item-BzodimsI.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-DU1jqVQi.js → createContext-DL2jaWH5.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-CZB0EDsa.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-DdNH-aW_.js → hasIn-C4Geudre.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CHmkkdfT.js → index-B5Sn7CtW.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Cj6P9Nst.js → index-B6w4fpSI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CQorP1Ng.js → index-BRj6YKuI.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-cYaWV4Mq.js → index-BX4pKudW.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-kVO_MDm-.js → index-BaTvPxY0.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D23xGYjM.js → index-BbWRxWM2.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Be4OtL-N.js → index-BeZhISVl.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-32ajXmTb.js → index-Bi6Zkv5M.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-NmEXVjzt.js → index-BvKoSvko.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-7ehytj1K.js → index-C3LzRvkw.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-gg3QPC3i.js → index-CAjyUjZl.js} +3 -3
  109. package/src/assets/web-panel/assets/index-CI7k8Sk9.js +1 -0
  110. package/src/assets/web-panel/assets/{index-CGiwfwB9.js → index-COxGCo8v.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DIVyTmZ3.js → index-CQBPhJlk.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CUoNAmE8.js → index-CQPOCB4h.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-t_OBzIP4.js → index-CTlmw_cr.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-VDM2KKo9.js → index-CVFXYmUe.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DftZuyA7.js → index-C_SxaIPI.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BcGwGIaR.js → index-CaKKxqCI.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-HjCvXHf5.js → index-CcgoKNXN.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-mgU3Q-lN.js → index-CczBR6Du.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DM5FmPZU.js → index-CeUnSSEP.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-D4l82Me1.js → index-CnwHp5Oi.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BwlIxAmX.js → index-DGBRBg3D.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BVLL-mq_.js → index-DHdD0Y7h.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BXKhhx9V.js → index-DMY1waPC.js} +1 -1
  124. package/src/assets/web-panel/assets/index-DPTHBSR8.js +1 -0
  125. package/src/assets/web-panel/assets/{index-LaivuXyF.js → index-DREx4gaS.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CtRPi4YC.js → index-DXQ2OtXt.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DRzBBdQ0.js → index-DZU7Y67X.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-PjAS0sIZ.js → index-DZtKnnTW.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-MM4Zz2OE.js → index-DlABpgCv.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BoFak7le.js → index-Dl_QAtzq.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-D1qIC4GR.js → index-DnBybYhx.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D4miNkWv.js → index-DoyIjk6r.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-DtBCCkC4.js → index-DvEnrn8M.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-BC4eGkar.js → index-DvOzjf2F.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-yZRuZpwK.js → index-EjW1R_a7.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-Dca_i7H8.js → index-F-SCUTee.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-DwLTRNdo.js → initDefaultProps-CW5mTl3w.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-BchZDXnh.js → motion-BtjIT9np.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-e8M-zZFl.js → move-DZv4fZAV.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-Ci40fQb3.js → mtc-parser-BwCix1O-.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-C7110_f2.js → omit-BW72HDUD.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-skteMgnM.js → pickAttrs-DBW2jcMc.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-MGZ-lh2B.js → placementArrow-D3YmWlIS.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-N4Sd7uy-.js → responsiveObserve-CL4-a-9Q.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-C7HDGRaa.js → slide-Cw9zIbXQ.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-BhRexlgy.js → statusUtils-mMLVioo-.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-ChEoQkN2.js → styleChecker-BK8857AH.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-CDbG1s2Y.js → useFlexGapSupport-C1iSaJij.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-CUIx0tcJ.js → useFs-D3riGOaP.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-bkO7L7ec.js → usePersonalDataHub-Bwqvmz4X.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-B_ZvABIG.js → vnode-Byk4zzkf.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-D5shjpS8.js → zoom-D7INFTp2.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +1 -1
  155. package/src/commands/mcp.js +119 -0
  156. package/src/commands/session-show.js +8 -10
  157. package/src/commands/session.js +60 -52
  158. package/src/data/changelog.json +40 -1
  159. package/src/gateways/ws/session-protocol.js +14 -4
  160. package/src/gateways/ws/ws-agent-handler.js +31 -0
  161. package/src/harness/jsonl-session-store.js +1147 -130
  162. package/src/harness/mcp-client.js +44 -11
  163. package/src/harness/session-index.js +80 -80
  164. package/src/harness/session-list-index.js +147 -2
  165. package/src/lib/background-interaction-journal.js +2 -2
  166. package/src/lib/command-lifecycle-report.js +516 -0
  167. package/src/lib/mcp-stdio-environment.js +48 -0
  168. package/src/lib/mcp-stdio-executable-identity.js +552 -17
  169. package/src/lib/mcp-stdio-package-materialization.js +1424 -0
  170. package/src/lib/packer/native-update-state.js +979 -1
  171. package/src/lib/packer/pack-update-applier.js +1050 -125
  172. package/src/lib/packer/pkg-config-generator.js +1 -0
  173. package/src/lib/paths.js +53 -0
  174. package/src/lib/process-execution-broker/index.js +180 -37
  175. package/src/lib/process-execution-broker/platform-sandbox.js +291 -24
  176. package/src/lib/recent-session.js +17 -17
  177. package/src/lib/secure-file-identity.js +47 -0
  178. package/src/lib/secure-fs.js +21 -2
  179. package/src/lib/session-anti-rollback-anchor.js +766 -0
  180. package/src/lib/session-host-lease.js +492 -0
  181. package/src/lib/session-persistence-failure.js +109 -0
  182. package/src/lib/session-picker.js +26 -3
  183. package/src/lib/session-search.js +25 -11
  184. package/src/lib/side-effect-ledger-store.js +2 -2
  185. package/src/lib/skill-execution-authority.js +13 -9
  186. package/src/lib/turn-binding-store.js +2 -2
  187. package/src/repl/agent-repl.js +309 -39
  188. package/src/runtime/agent-core.js +88 -5
  189. package/src/runtime/cleanup-deadline.js +143 -0
  190. package/src/runtime/headless-runner.js +357 -139
  191. package/src/runtime/headless-stream.js +207 -28
  192. package/src/runtime/output-backpressure.js +53 -0
  193. package/src/assets/web-panel/assets/MobileProjects-C8qfktxn.js +0 -1
  194. package/src/assets/web-panel/assets/Providers-DDCf_nIw.js +0 -1
  195. package/src/assets/web-panel/assets/devWarning-CP-jEUyw.js +0 -1
  196. package/src/assets/web-panel/assets/index-CzrCHrFy.js +0 -1
  197. package/src/assets/web-panel/assets/index-OJsGskZw.js +0 -1
@@ -0,0 +1,492 @@
1
+ /**
2
+ * Durable same-machine lease and fencing authority for canonical sessions.
3
+ *
4
+ * A lease is cooperative authority for production hosts, not a same-UID
5
+ * sandbox. Every process sharing CHAINLESSCHAIN_HOME observes the same strict
6
+ * state file. A monotonically increasing fencing token prevents an expired or
7
+ * crashed host from publishing through guarded session-store write paths after
8
+ * a successor takes over.
9
+ */
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import crypto from "node:crypto";
14
+ import { getStatePath } from "./paths.js";
15
+ import {
16
+ mutateSecurityStore,
17
+ readSecurityStore,
18
+ } from "./durable-security-store.js";
19
+ import { withFileLock } from "./with-file-lock.js";
20
+
21
+ export const SESSION_HOST_LEASE_HELD_CODE = "CC_SESSION_HOST_LEASE_HELD";
22
+ export const SESSION_HOST_LEASE_FENCED_CODE = "CC_SESSION_HOST_LEASE_FENCED";
23
+ export const SESSION_HOST_LEASE_UNAVAILABLE_CODE =
24
+ "CC_SESSION_HOST_LEASE_UNAVAILABLE";
25
+
26
+ const STORE_VERSION = 1;
27
+ const DEFAULT_TTL_MS = 30_000;
28
+ const DEFAULT_HEARTBEAT_MS = 5_000;
29
+ const MAX_SESSION_ID_BYTES = 1024;
30
+ const HOST_KIND_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
31
+ const LEASE_ID_PATTERN = /^lease-[0-9a-f-]{36}$/;
32
+
33
+ function leaseError(code, message, details = {}, cause = null) {
34
+ const error = new Error(message, cause ? { cause } : undefined);
35
+ error.name = "SessionHostLeaseError";
36
+ error.code = code;
37
+ Object.assign(error, details);
38
+ return error;
39
+ }
40
+
41
+ function canonicalSessionId(value) {
42
+ if (
43
+ typeof value !== "string" ||
44
+ value.length === 0 ||
45
+ value.includes("\0") ||
46
+ Buffer.byteLength(value, "utf8") > MAX_SESSION_ID_BYTES
47
+ ) {
48
+ throw new TypeError("session host lease requires a bounded session id");
49
+ }
50
+ return value;
51
+ }
52
+
53
+ function canonicalHostKind(value) {
54
+ const kind = String(value || "unknown").toLowerCase();
55
+ if (!HOST_KIND_PATTERN.test(kind)) {
56
+ throw new TypeError("session host kind is invalid");
57
+ }
58
+ return kind;
59
+ }
60
+
61
+ function positiveInteger(value, label) {
62
+ const number = Number(value);
63
+ if (!Number.isSafeInteger(number) || number < 1) {
64
+ throw new TypeError(`${label} must be a positive integer`);
65
+ }
66
+ return number;
67
+ }
68
+
69
+ function normalizeActiveLease(value) {
70
+ if (value === null) return null;
71
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
72
+ throw new TypeError("active session host lease must be an object or null");
73
+ }
74
+ if (
75
+ typeof value.leaseId !== "string" ||
76
+ !LEASE_ID_PATTERN.test(value.leaseId) ||
77
+ !Number.isSafeInteger(value.fencingToken) ||
78
+ value.fencingToken < 1 ||
79
+ !Number.isSafeInteger(value.ownerPid) ||
80
+ value.ownerPid < 1 ||
81
+ typeof value.hostKind !== "string" ||
82
+ !HOST_KIND_PATTERN.test(value.hostKind) ||
83
+ !Number.isSafeInteger(value.acquiredAtMs) ||
84
+ value.acquiredAtMs < 0 ||
85
+ !Number.isSafeInteger(value.renewedAtMs) ||
86
+ value.renewedAtMs < value.acquiredAtMs ||
87
+ !Number.isSafeInteger(value.expiresAtMs) ||
88
+ value.expiresAtMs < value.renewedAtMs
89
+ ) {
90
+ throw new TypeError("active session host lease is invalid");
91
+ }
92
+ return Object.freeze({
93
+ leaseId: value.leaseId,
94
+ fencingToken: value.fencingToken,
95
+ ownerPid: value.ownerPid,
96
+ hostKind: value.hostKind,
97
+ acquiredAtMs: value.acquiredAtMs,
98
+ renewedAtMs: value.renewedAtMs,
99
+ expiresAtMs: value.expiresAtMs,
100
+ });
101
+ }
102
+
103
+ function validateStore(value, sessionId, { missing = false } = {}) {
104
+ if (missing && Object.keys(value).length === 0) {
105
+ return Object.freeze({
106
+ version: STORE_VERSION,
107
+ sessionId,
108
+ lastFencingToken: 0,
109
+ active: null,
110
+ });
111
+ }
112
+ if (
113
+ value?.version !== STORE_VERSION ||
114
+ value?.sessionId !== sessionId ||
115
+ !Number.isSafeInteger(value?.lastFencingToken) ||
116
+ value.lastFencingToken < 0
117
+ ) {
118
+ throw new TypeError("session host lease store is invalid");
119
+ }
120
+ const active = normalizeActiveLease(value.active);
121
+ if (active && active.fencingToken !== value.lastFencingToken) {
122
+ throw new TypeError("active lease does not match the last fencing token");
123
+ }
124
+ return Object.freeze({
125
+ version: STORE_VERSION,
126
+ sessionId,
127
+ lastFencingToken: value.lastFencingToken,
128
+ active,
129
+ });
130
+ }
131
+
132
+ function sameLease(left, right) {
133
+ return Boolean(
134
+ left &&
135
+ right &&
136
+ left.leaseId === right.leaseId &&
137
+ left.fencingToken === right.fencingToken &&
138
+ left.ownerPid === right.ownerPid,
139
+ );
140
+ }
141
+
142
+ function processAlive(pid) {
143
+ try {
144
+ process.kill(pid, 0);
145
+ return true;
146
+ } catch (error) {
147
+ return error?.code === "EPERM";
148
+ }
149
+ }
150
+
151
+ function publicLease(active) {
152
+ return Object.freeze({ ...active });
153
+ }
154
+
155
+ export function getSessionHostLeaseRoot() {
156
+ return path.join(getStatePath(), "session-host-leases");
157
+ }
158
+
159
+ export class SessionHostLeaseAuthority {
160
+ constructor({
161
+ stateRoot = getSessionHostLeaseRoot(),
162
+ now = () => Date.now(),
163
+ isProcessAlive = processAlive,
164
+ createLeaseId = () => `lease-${crypto.randomUUID()}`,
165
+ setIntervalFn = setInterval,
166
+ clearIntervalFn = clearInterval,
167
+ lock = withFileLock,
168
+ } = {}) {
169
+ if (!path.isAbsolute(stateRoot)) {
170
+ throw new TypeError("session host lease root must be absolute");
171
+ }
172
+ this.stateRoot = path.resolve(stateRoot);
173
+ this._now = now;
174
+ this._isProcessAlive = isProcessAlive;
175
+ this._createLeaseId = createLeaseId;
176
+ this._setInterval = setIntervalFn;
177
+ this._clearInterval = clearIntervalFn;
178
+ this._lock = lock;
179
+ this._local = new Map();
180
+ }
181
+
182
+ pathFor(sessionId) {
183
+ const id = canonicalSessionId(sessionId);
184
+ const digest = crypto.createHash("sha256").update(id).digest("hex");
185
+ return path.join(this.stateRoot, `${digest}.json`);
186
+ }
187
+
188
+ _key(sessionId) {
189
+ return `${this.pathFor(sessionId)}\0${sessionId}`;
190
+ }
191
+
192
+ _read(filePath, sessionId) {
193
+ const existed = fs.existsSync(filePath);
194
+ return validateStore(
195
+ readSecurityStore(filePath, "Session host lease authority"),
196
+ sessionId,
197
+ { missing: !existed },
198
+ );
199
+ }
200
+
201
+ _unavailable(sessionId, filePath, cause) {
202
+ if (
203
+ cause?.code === SESSION_HOST_LEASE_HELD_CODE ||
204
+ cause?.code === SESSION_HOST_LEASE_FENCED_CODE
205
+ ) {
206
+ return cause;
207
+ }
208
+ return leaseError(
209
+ SESSION_HOST_LEASE_UNAVAILABLE_CODE,
210
+ `Session host lease authority is unavailable for ${sessionId}`,
211
+ { sessionId, filePath },
212
+ cause,
213
+ );
214
+ }
215
+
216
+ _lose(record, cause) {
217
+ if (!record || record.released) return;
218
+ record.released = true;
219
+ if (record.timer) this._clearInterval(record.timer);
220
+ record.timer = null;
221
+ this._local.delete(record.key);
222
+ const reason =
223
+ cause?.code === SESSION_HOST_LEASE_FENCED_CODE
224
+ ? cause
225
+ : this._unavailable(record.sessionId, record.filePath, cause);
226
+ if (!record.controller.signal.aborted) record.controller.abort(reason);
227
+ }
228
+
229
+ acquire(
230
+ sessionId,
231
+ {
232
+ hostKind = "unknown",
233
+ ttlMs = DEFAULT_TTL_MS,
234
+ heartbeatMs = DEFAULT_HEARTBEAT_MS,
235
+ } = {},
236
+ ) {
237
+ const id = canonicalSessionId(sessionId);
238
+ const kind = canonicalHostKind(hostKind);
239
+ const ttl = Math.max(1_000, positiveInteger(ttlMs, "lease ttl"));
240
+ const heartbeat = Math.max(
241
+ 250,
242
+ Math.min(ttl - 1, positiveInteger(heartbeatMs, "heartbeat interval")),
243
+ );
244
+ const filePath = this.pathFor(id);
245
+ const key = this._key(id);
246
+ if (this._local.has(key)) {
247
+ throw leaseError(
248
+ SESSION_HOST_LEASE_HELD_CODE,
249
+ `This process already hosts session ${id}`,
250
+ { sessionId: id, filePath },
251
+ );
252
+ }
253
+
254
+ let active;
255
+ try {
256
+ active = mutateSecurityStore(
257
+ filePath,
258
+ "Session host lease authority",
259
+ (draft) => {
260
+ const snapshot = validateStore(draft, id, {
261
+ missing: !fs.existsSync(filePath),
262
+ });
263
+ const now = Math.trunc(this._now());
264
+ const incumbent = snapshot.active;
265
+ // Never take authority from a process that the OS still reports as
266
+ // alive, even after its heartbeat TTL. It may be blocked inside an
267
+ // external side effect and resume later. Expiry fences that host's
268
+ // next guarded transition; takeover additionally requires proven
269
+ // process death so two executors cannot overlap.
270
+ if (incumbent && this._isProcessAlive(incumbent.ownerPid)) {
271
+ throw leaseError(
272
+ SESSION_HOST_LEASE_HELD_CODE,
273
+ `Session ${id} is already hosted by another live process`,
274
+ {
275
+ sessionId: id,
276
+ filePath,
277
+ incumbent: publicLease(incumbent),
278
+ },
279
+ );
280
+ }
281
+ const fencingToken = snapshot.lastFencingToken + 1;
282
+ const next = {
283
+ leaseId: this._createLeaseId(),
284
+ fencingToken,
285
+ ownerPid: process.pid,
286
+ hostKind: kind,
287
+ acquiredAtMs: now,
288
+ renewedAtMs: now,
289
+ expiresAtMs: now + ttl,
290
+ };
291
+ normalizeActiveLease(next);
292
+ draft.version = STORE_VERSION;
293
+ draft.sessionId = id;
294
+ draft.lastFencingToken = fencingToken;
295
+ draft.active = next;
296
+ return next;
297
+ },
298
+ { lock: this._lock, timeoutMs: 30_000, staleMs: 30_000 },
299
+ );
300
+ } catch (cause) {
301
+ throw this._unavailable(id, filePath, cause);
302
+ }
303
+
304
+ const controller = new AbortController();
305
+ const record = {
306
+ key,
307
+ sessionId: id,
308
+ filePath,
309
+ ttlMs: ttl,
310
+ heartbeatMs: heartbeat,
311
+ active: normalizeActiveLease(active),
312
+ controller,
313
+ timer: null,
314
+ released: false,
315
+ };
316
+ this._local.set(key, record);
317
+
318
+ const renew = () => {
319
+ if (record.released) {
320
+ throw leaseError(
321
+ SESSION_HOST_LEASE_FENCED_CODE,
322
+ `Session host lease is no longer active for ${id}`,
323
+ { sessionId: id, fencingToken: record.active.fencingToken },
324
+ );
325
+ }
326
+ try {
327
+ const renewed = mutateSecurityStore(
328
+ filePath,
329
+ "Session host lease authority",
330
+ (draft) => {
331
+ const snapshot = validateStore(draft, id);
332
+ if (!sameLease(snapshot.active, record.active)) {
333
+ throw leaseError(
334
+ SESSION_HOST_LEASE_FENCED_CODE,
335
+ `Session host lease was superseded for ${id}`,
336
+ {
337
+ sessionId: id,
338
+ fencingToken: record.active.fencingToken,
339
+ activeFencingToken: snapshot.active?.fencingToken ?? null,
340
+ },
341
+ );
342
+ }
343
+ const now = Math.trunc(this._now());
344
+ if (snapshot.active.expiresAtMs <= now) {
345
+ throw leaseError(
346
+ SESSION_HOST_LEASE_FENCED_CODE,
347
+ `Session host lease expired for ${id}`,
348
+ { sessionId: id, fencingToken: record.active.fencingToken },
349
+ );
350
+ }
351
+ const next = {
352
+ ...snapshot.active,
353
+ renewedAtMs: now,
354
+ expiresAtMs: now + ttl,
355
+ };
356
+ draft.active = next;
357
+ return next;
358
+ },
359
+ { lock: this._lock, timeoutMs: 30_000, staleMs: 30_000 },
360
+ );
361
+ record.active = normalizeActiveLease(renewed);
362
+ return publicLease(record.active);
363
+ } catch (cause) {
364
+ const error = this._unavailable(id, filePath, cause);
365
+ this._lose(record, error);
366
+ throw error;
367
+ }
368
+ };
369
+
370
+ const release = () => {
371
+ if (record.released) return false;
372
+ if (record.timer) this._clearInterval(record.timer);
373
+ record.timer = null;
374
+ let released = false;
375
+ try {
376
+ released = mutateSecurityStore(
377
+ filePath,
378
+ "Session host lease authority",
379
+ (draft) => {
380
+ const snapshot = validateStore(draft, id);
381
+ if (!sameLease(snapshot.active, record.active)) return false;
382
+ draft.version = STORE_VERSION;
383
+ draft.sessionId = id;
384
+ draft.lastFencingToken = snapshot.lastFencingToken;
385
+ draft.active = null;
386
+ return true;
387
+ },
388
+ { lock: this._lock, timeoutMs: 30_000, staleMs: 30_000 },
389
+ );
390
+ } catch (cause) {
391
+ const error = this._unavailable(id, filePath, cause);
392
+ this._lose(record, error);
393
+ throw error;
394
+ }
395
+ record.released = true;
396
+ this._local.delete(key);
397
+ return released;
398
+ };
399
+
400
+ record.timer = this._setInterval(() => {
401
+ try {
402
+ renew();
403
+ } catch {
404
+ // renew() already aborts and retires the local lease.
405
+ }
406
+ }, heartbeat);
407
+ record.timer?.unref?.();
408
+
409
+ return Object.freeze({
410
+ sessionId: id,
411
+ leaseId: record.active.leaseId,
412
+ fencingToken: record.active.fencingToken,
413
+ signal: controller.signal,
414
+ renew,
415
+ assert: () => this.withWriteAuthority(id, (authority) => authority),
416
+ release,
417
+ });
418
+ }
419
+
420
+ withWriteAuthority(sessionId, task) {
421
+ if (typeof task !== "function") {
422
+ throw new TypeError("session host write authority callback is required");
423
+ }
424
+ const id = canonicalSessionId(sessionId);
425
+ const filePath = this.pathFor(id);
426
+ if (!fs.existsSync(filePath)) return task(null);
427
+ try {
428
+ return this._lock(
429
+ filePath,
430
+ () => {
431
+ const snapshot = this._read(filePath, id);
432
+ if (!snapshot.active) return task(null);
433
+ const record = this._local.get(this._key(id));
434
+ const now = Math.trunc(this._now());
435
+ if (
436
+ !record ||
437
+ record.released ||
438
+ !sameLease(snapshot.active, record.active) ||
439
+ snapshot.active.expiresAtMs <= now
440
+ ) {
441
+ const error = leaseError(
442
+ SESSION_HOST_LEASE_FENCED_CODE,
443
+ `Session write is fenced by another or expired host lease: ${id}`,
444
+ {
445
+ sessionId: id,
446
+ activeFencingToken: snapshot.active.fencingToken,
447
+ localFencingToken: record?.active?.fencingToken ?? null,
448
+ },
449
+ );
450
+ if (record) this._lose(record, error);
451
+ throw error;
452
+ }
453
+ return task(
454
+ Object.freeze({
455
+ leaseId: snapshot.active.leaseId,
456
+ fencingToken: snapshot.active.fencingToken,
457
+ ownerPid: snapshot.active.ownerPid,
458
+ hostKind: snapshot.active.hostKind,
459
+ }),
460
+ );
461
+ },
462
+ {
463
+ timeoutMs: 30_000,
464
+ staleMs: 30_000,
465
+ retryMs: 1,
466
+ maxRetryMs: 8,
467
+ retryJitterMs: 4,
468
+ failIfUnavailable: true,
469
+ },
470
+ );
471
+ } catch (cause) {
472
+ throw this._unavailable(id, filePath, cause);
473
+ }
474
+ }
475
+ }
476
+
477
+ const defaultAuthority = new SessionHostLeaseAuthority();
478
+
479
+ export function acquireSessionHostLease(sessionId, options) {
480
+ return defaultAuthority.acquire(sessionId, options);
481
+ }
482
+
483
+ export function withSessionHostWriteAuthority(sessionId, task) {
484
+ return defaultAuthority.withWriteAuthority(sessionId, task);
485
+ }
486
+
487
+ export function assertSessionHostWriteAuthority(sessionId) {
488
+ return defaultAuthority.withWriteAuthority(
489
+ sessionId,
490
+ (authority) => authority,
491
+ );
492
+ }
@@ -0,0 +1,109 @@
1
+ export const SESSION_PERSISTENCE_FAILURE_SCHEMA =
2
+ "chainlesschain.session-persistence-failure.v1";
3
+ export const SESSION_PERSISTENCE_FAILURE_CODE = "CC_SESSION_PERSISTENCE_FAILED";
4
+
5
+ const STORAGE_FAILURE_CODES = new Set(["ENOSPC", "EROFS"]);
6
+ const COMMIT_STATES = new Set(["not-committed", "unknown", "committed"]);
7
+ const OPERATIONS = new Set([
8
+ "append-event",
9
+ "append-authority-event",
10
+ "transcript-append",
11
+ "transcript-settlement",
12
+ "session-start",
13
+ "user-turn-append",
14
+ "assistant-turn-append",
15
+ ]);
16
+ const PHASES = new Set(["before-model", "after-model", "unspecified"]);
17
+
18
+ function normalizedOperation(value) {
19
+ return OPERATIONS.has(value) ? value : "unknown";
20
+ }
21
+
22
+ function normalizedPhase(value) {
23
+ return PHASES.has(value) ? value : "unspecified";
24
+ }
25
+
26
+ function storageCode(error) {
27
+ const seen = new Set();
28
+ let current = error;
29
+ for (let depth = 0; current && depth < 8; depth += 1) {
30
+ if (seen.has(current)) break;
31
+ seen.add(current);
32
+ for (const code of [current.fsCode, current.code]) {
33
+ if (STORAGE_FAILURE_CODES.has(code)) return code;
34
+ }
35
+ current = current.cause;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function normalizedCommitState(value, fsCode) {
41
+ if (COMMIT_STATES.has(value)) return value;
42
+ // Opening an append target on a read-only filesystem cannot publish the
43
+ // requested record. ENOSPC may be reported after a short write, so callers
44
+ // must verify the transcript instead of retrying blindly.
45
+ return fsCode === "EROFS" ? "not-committed" : "unknown";
46
+ }
47
+
48
+ function retryableFor(fsCode, commitState) {
49
+ return fsCode === "ENOSPC" && commitState === "not-committed";
50
+ }
51
+
52
+ export function isSessionPersistenceFailure(error) {
53
+ return (
54
+ error?.code === SESSION_PERSISTENCE_FAILURE_CODE &&
55
+ STORAGE_FAILURE_CODES.has(error?.fsCode) &&
56
+ COMMIT_STATES.has(error?.commitState)
57
+ );
58
+ }
59
+
60
+ export function createSessionPersistenceFailure(
61
+ cause,
62
+ { sessionId = null, operation = "append-event", commitState } = {},
63
+ ) {
64
+ if (isSessionPersistenceFailure(cause)) {
65
+ const expectedRetryable = retryableFor(cause.fsCode, cause.commitState);
66
+ if (cause.retryable === expectedRetryable) return cause;
67
+ const normalized = new Error(
68
+ `Session persistence failed during ${normalizedOperation(cause.operation)} (${cause.fsCode})`,
69
+ { cause },
70
+ );
71
+ normalized.code = SESSION_PERSISTENCE_FAILURE_CODE;
72
+ normalized.fsCode = cause.fsCode;
73
+ normalized.operation = normalizedOperation(cause.operation);
74
+ normalized.commitState = cause.commitState;
75
+ normalized.retryable = expectedRetryable;
76
+ if (cause.sessionId) normalized.sessionId = cause.sessionId;
77
+ return normalized;
78
+ }
79
+ const fsCode = storageCode(cause);
80
+ if (!fsCode) return cause;
81
+
82
+ const error = new Error(
83
+ `Session persistence failed during ${normalizedOperation(operation)} (${fsCode})`,
84
+ { cause },
85
+ );
86
+ error.code = SESSION_PERSISTENCE_FAILURE_CODE;
87
+ error.fsCode = fsCode;
88
+ error.operation = normalizedOperation(operation);
89
+ error.commitState = normalizedCommitState(commitState, fsCode);
90
+ // ENOSPC is retryable only when the failed append is known not to have
91
+ // committed. An unknown short-write result requires transcript verification
92
+ // first; exposing it as retryable would invite a duplicate event.
93
+ error.retryable = retryableFor(fsCode, error.commitState);
94
+ if (sessionId) error.sessionId = sessionId;
95
+ return error;
96
+ }
97
+
98
+ export function projectSessionPersistenceFailure(error, { phase } = {}) {
99
+ if (!isSessionPersistenceFailure(error)) return null;
100
+ return Object.freeze({
101
+ schema: SESSION_PERSISTENCE_FAILURE_SCHEMA,
102
+ code: SESSION_PERSISTENCE_FAILURE_CODE,
103
+ fs_code: error.fsCode,
104
+ operation: normalizedOperation(error.operation),
105
+ phase: normalizedPhase(phase),
106
+ commit_state: error.commitState,
107
+ retryable: retryableFor(error.fsCode, error.commitState),
108
+ });
109
+ }
@@ -22,12 +22,25 @@ export function formatSessionChoice(s) {
22
22
  const when = chalk.gray(s.updated_at || "");
23
23
  const store = s._store === "jsonl" ? " " + chalk.yellow("[JSONL]") : "";
24
24
  return {
25
- name: `${title} ${msgs} ${when}${store}`,
25
+ name: `${title} ${msgs} ${when}${store}${s._blocked ? chalk.red(` [${s._presence || "unavailable"}]`) : ""}`,
26
26
  value: s.id,
27
27
  short: String(s.id).slice(0, 12),
28
+ ...(s._blocked
29
+ ? { disabled: `canonical session is ${s._presence || "unavailable"}` }
30
+ : {}),
28
31
  };
29
32
  }
30
33
 
34
+ function blockedSessionError(session) {
35
+ const error = new Error(
36
+ `Canonical session ${session.id} is not resumable (${session._presence || "unavailable"})`,
37
+ );
38
+ error.code = "SESSION_CANONICAL_UNAVAILABLE";
39
+ error.sessionId = session.id;
40
+ error.presence = session._presence || "unavailable";
41
+ return error;
42
+ }
43
+
31
44
  /**
32
45
  * Resolve which recent session to resume.
33
46
  *
@@ -51,18 +64,28 @@ export async function pickRecentSession(ctx, opts = {}, deps = {}) {
51
64
 
52
65
  // No interactive prompt possible/wanted → newest wins (claude --continue).
53
66
  if (!isTTY || sessions.length === 1 || opts.noPicker) {
67
+ if (sessions[0]._blocked) throw blockedSessionError(sessions[0]);
54
68
  return { id: sessions[0].id, sessions, picked: false };
55
69
  }
56
70
 
71
+ if (!sessions.some((session) => !session._blocked)) {
72
+ throw blockedSessionError(sessions[0]);
73
+ }
74
+
57
75
  const select = deps.select || (await import("@inquirer/prompts")).select;
58
76
  try {
59
77
  const id = await select({
60
78
  message: opts.message || "Resume which session?",
61
79
  choices: sessions.map(formatSessionChoice),
62
80
  });
81
+ const selected = sessions.find((session) => session.id === id);
82
+ if (selected?._blocked) throw blockedSessionError(selected);
63
83
  return { id, sessions, picked: true };
64
- } catch {
84
+ } catch (error) {
85
+ if (error?.code === "SESSION_CANONICAL_UNAVAILABLE") throw error;
65
86
  // Ctrl-C / non-interactive environment → fall back to most recent.
66
- return { id: sessions[0].id, sessions, picked: false };
87
+ const fallback = sessions.find((session) => !session._blocked);
88
+ if (!fallback) throw blockedSessionError(sessions[0]);
89
+ return { id: fallback.id, sessions, picked: false };
67
90
  }
68
91
  }