chainlesschain 0.162.197 → 0.162.200

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 (233) hide show
  1. package/bin/chainlesschain.js +14 -6
  2. package/package.json +4 -1
  3. package/scripts/command-lifecycle-report.mjs +94 -0
  4. package/src/assets/web-panel/assets/{AIOps-D3Gu1a5d.js → AIOps-DcQ0bWKy.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-DrwAoKaz.js → ActionButton-GecwfFPt.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C7Q-gd_3.js → Analytics-CD6iSt9v.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-DgBtae-n.js → AppLayout-CDEtuEYf.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-pQ6LcA-W.js → Artifacts-DS3jAjeW.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-DK61tocB.js → Audit-CcSJA048.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-BOKgDFRh.js → BackgroundAgents-DRo9Morb.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-Bo9G4vTP.js → Backup-Vw5MrWft.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-Do_dE9_v.js → BaseInput-DNHIu823.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-COVvZzOL.js → Chat-DvAsO8-b.js} +4 -4
  14. package/src/assets/web-panel/assets/ChatBubbleRenderer-9aDuObay.js +1 -0
  15. package/src/assets/web-panel/assets/{Checkbox-Z9MJMolt.js → Checkbox-CKCa7tvn.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-BVzWKVbf.js → Codegen-CIFvZdaT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-C8MnYcbT.js → Col-C79yz7bK.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-DQxqw15l.js → Community-DVvZskLJ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-BAUmrh3y.js → Compact-oaew61hV.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-jPAkQ3zK.js → Compliance-DqA7wmRx.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-BBQyxbh8.js → Cowork-B-qGadxa.js} +3 -3
  22. package/src/assets/web-panel/assets/{Cron-W5koq8Z5.js → Cron-CUlCl4sS.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-BQhnu5hS.js → Crosschain-CNOlr9DU.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-DD7PlI18.js → DID-DJqFCT_3.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-w_ILuwKI.js → Dashboard-CznGpphf.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-qiDUAyAo.js → Dropdown-_cJw218E.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-PDeBcI5H.js → EmailListRenderer-Dr-1BZ9C.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-SuArscRM.js → FamilyGuardDashboard-Dxf4xtSk.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-BCovZaUn.js → Federation-DOiG3i_1.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-DjlFdVFk.js → FormItemContext-BzbMcFB9.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-DoEZ7fcS.js → GenericCardRenderer-VbBrYttP.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-CV0-6GPI.js → Git-CLG8E6ED.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-B_SFN50w.js → Governance-LGJQfk_q.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-heI0Eo6k.js → Inference-DlQKx9lF.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-Cz5INS5p.js → KnowledgeGraph-DU8vEouV.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-BSFxiHkg.js → Logs-FZIQOdln.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BJNWIrbw.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-0TWTpE7k.js → Marketplace-C_xd-zZI.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-CV4-wBLm.js → McpTools-q-YktphS.js} +5 -5
  40. package/src/assets/web-panel/assets/{Memory-x5bH5rw5.js → Memory-Cq9OhDin.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-DaBmUu9G.js → MobileBridge-B03_R98T.js} +1 -1
  42. package/src/assets/web-panel/assets/MobileProjects-DZrk-qmF.js +1 -0
  43. package/src/assets/web-panel/assets/{Mtc-B_zKEBif.js → Mtc-BidCKW28.js} +5 -5
  44. package/src/assets/web-panel/assets/{MtcAudit-BCfKD2yg.js → MtcAudit-r-iaMVM7.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-DgYfj6c_.js → Multisig-CgTUFvff.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-Dz09lFUm.js → NLProgramming-B7NvzfAV.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-Cuj6ugBE.js → Notes-80CWNqJI.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-DK2WXjhg.js → NotificationSettings-0H6xdiY3.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-D1IAOQoo.js → OrderTableRenderer-CKP4AFWy.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-9fqnPvTw.js → Organization-B8sxCV_h.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-n5xHkhow.js → Overflow-CW9JBBhQ.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-B5AUWwH-.js → P2P-bdSQV7oZ.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-CoUDV6dy.js → PdhVaultBrowser-DQad42nW.js} +4 -4
  54. package/src/assets/web-panel/assets/{Permissions-DPRmQf_H.js → Permissions-CueexxTR.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-B37bsbEN.js → PersonalDataHub-fqxflhR8.js} +6 -6
  56. package/src/assets/web-panel/assets/{Pipeline-CCAuPqu9.js → Pipeline-KFwwnIMC.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-B81msZdv.js → Privacy-ClnD9buY.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-B-Py2NbF.js → ProjectInit-CZEz6sm3.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-CYsneiRB.js → ProjectSettings-B6NzpmOI.js} +2 -2
  60. package/src/assets/web-panel/assets/{Projects-Y975Mh6R.js → Projects-DYyuwPCs.js} +1 -1
  61. package/src/assets/web-panel/assets/Providers-C3tz_jdh.js +1 -0
  62. package/src/assets/web-panel/assets/{QrScannerModal-Bj_6gJ0l.js → QrScannerModal-BzuOBbqB.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-BgpBCMPE.js → QuickAsk-CmNO9qSj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-DhUW3YUA.js → Recommend-VJNj3_NF.js} +1 -1
  65. package/src/assets/web-panel/assets/{RemoteSession-DdeyfbkA.js → RemoteSession-CrbdbSBF.js} +2 -2
  66. package/src/assets/web-panel/assets/{Reputation-fxW421xd.js → Reputation-DTyMd45j.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-De73w1og.js → Row-DHrQ-Icz.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-3US_Q_lj.js → RssFeed-DqWxY9c5.js} +2 -2
  69. package/src/assets/web-panel/assets/{Search-DbZ_DU74.js → Search-ctF1Ks3J.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-DoIU1vMF.js → Security-BrQz8uo5.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-DB_RGqpc.js → Services-D4scVFar.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DnzLaayC.js → Skeleton-oUScaOjv.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-VzVXHENF.js → Skills-CL-y0eLt.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-DMfvjCAI.js → Sla-DQExhwXj.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-DQQOYWbF.js → SpeechSettings-iNp_I7kN.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-Bmx3FG1P.js → SyncSettings-CTDlMo0A.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-DOQyfb-2.js → Tasks-4kb8l3sc.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-D6hTRT7c.js → Templates-BXcL9oXW.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-CTPQZAzs.js → Tenant-DLE8eS0D.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-B7vII3oH.js → Terminal-DGpUZPPK.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-BkiFJeXV.js → TimelineRenderer-C1pWXGPC.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-BPPdQMNo.js → Tokens-BYfbfqhZ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-BxG8JAEI.js → Trigger-K9YfLOCw.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DHD7nMpM.js → Trust-Ctkq77Hy.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-B4BJpU47.js → UkeySign-C6_CSe9J.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-CRiGpFx2.js → VideoEditing-Be3rpOWs.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-BSDSNiem.js → Wallet-D0UIplxk.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-DLTUh5Zv.js → WebAuthn-CF9bM9I3.js} +5 -5
  89. package/src/assets/web-panel/assets/{WorkflowEditor-tW2MwjhD.js → WorkflowEditor-D0H1XVPY.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-SgsLQ3FC.js → chat-Cgzzbe0o.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-BHjjLIKO.js → colors-BwNxEVS0.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-VceErdKq.js → compact-item-D60Z4Hhs.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-BZ332MYl.js → createContext-MFk58XB0.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-BJiw369i.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-CBTjJOjn.js → hasIn-CRj2SvGO.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DaCCy6lq.js → index--s45_1cb.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-CMVvxIS8.js → index--tquQ2ZG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BhXZEUAc.js → index-1T1rhokn.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BQSmZGgs.js → index-B-M82_rx.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BeiqkxJG.js → index-BE_el1Zs.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-29wVzi-k.js → index-BhRkR7IO.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-iYQNU9za.js → index-Bo0y96-K.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BRWhLbWF.js → index-CJgu5JUK.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BPXFLYdW.js → index-CLWwdebO.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Cr4rjlW5.js → index-CNAK5QG_.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CaIS_TNX.js → index-CQjJjPRD.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-1n7xjKDU.js → index-CTDbAfae.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BETEjuNr.js → index-CVI_9sRK.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-By5jBtkV.js → index-CY14oMQK.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DgPvuIn2.js → index-CbiTJkZf.js} +1 -1
  111. package/src/assets/web-panel/assets/index-CiMouosn.js +1 -0
  112. package/src/assets/web-panel/assets/{index-CawpdGTW.js → index-CqWpxMqx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-_PHrIO4O.js → index-CvB8lYUR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BOtvPdHt.js → index-D51XwtCY.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CrUgDQkf.js → index-D6HvK_1O.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BY1wexYG.js → index-D9jVIwLJ.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-C_B9rFQE.js → index-DA4tE98u.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-B5KAGxNo.js → index-DBwOtXw0.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BC1xuQgX.js → index-DKdNzTIb.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-B2u26AS3.js → index-DRySdmvN.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DePOeMDM.js → index-DWzyTev7.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DXcKlwy7.js +1 -0
  123. package/src/assets/web-panel/assets/{index-BTlc-Bve.js → index-DYjs7jAP.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CLc19alH.js → index-DbROzMBp.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-fU7wNHDB.js → index-DhFtk5PR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D_SePDWj.js → index-Di2AbMLg.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-vIXOpcjD.js → index-DvmocQsM.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BcUenmCX.js → index-Dw-o0zEF.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DUkVN794.js → index-DysPoP38.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CtjLcCNU.js → index-LX9pJxm_.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-B40dQn0G.js → index-OI_fXWx0.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-FJ3zC9LK.js → index-eDTOX8Xj.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-DdefYBSw.js → index-gWKDsTzF.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-CpumS98o.js → index-q_GDUVAA.js} +1 -1
  135. package/src/assets/web-panel/assets/{initDefaultProps-D1oZNz0D.js → initDefaultProps-C61kZqEd.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-C8k6doMO.js → motion-DZnT5U1L.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-DJ4qSbYt.js → move-L8wzwbel.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-CdpJD85j.js → mtc-parser-DLdYSejc.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-B0V4slKA.js → omit-D53cbS59.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-Di5NiW1F.js → pickAttrs-Bbc5HPtu.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-B5OMHgEM.js → placementArrow-BkvmO860.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-C6U9IWql.js → responsiveObserve-u2y1ryS1.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-BMyTH3rN.js → slide-BBqj7svU.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-C7d6BG7Z.js → statusUtils-GN0mqu2d.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-CR50QnQy.js → styleChecker-BvwPKoBT.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-DBEeoMb8.js → useFlexGapSupport-CCt8tnfY.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-3r099cjv.js → useFs-DXAxl2SV.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-C5IYNdTK.js → usePersonalDataHub-DihlR2dj.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-DmEOLRHf.js → vnode-D7BOX2gi.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-DnpUiVCB.js → zoom-CVbO-nL7.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +4 -4
  153. package/src/commands/agent.js +3 -3
  154. package/src/commands/background-session.js +145 -0
  155. package/src/commands/mcp.js +244 -7
  156. package/src/commands/serve.js +1 -1
  157. package/src/commands/session-show.js +129 -0
  158. package/src/commands/session.js +97 -145
  159. package/src/commands/skill.js +87 -50
  160. package/src/data/changelog.json +40 -1
  161. package/src/gateways/ws/session-protocol.js +14 -4
  162. package/src/gateways/ws/ws-agent-handler.js +39 -1
  163. package/src/harness/jsonl-session-store.js +1547 -127
  164. package/src/harness/mcp-client.js +2690 -409
  165. package/src/harness/session-index.js +80 -80
  166. package/src/harness/session-list-index.js +210 -7
  167. package/src/lazy-dispatch.js +76 -8
  168. package/src/lib/abort-utils.js +68 -0
  169. package/src/lib/agent-core.js +1 -59
  170. package/src/lib/background-interaction-journal.js +2 -2
  171. package/src/lib/chat-core.js +24 -6
  172. package/src/lib/command-lifecycle-report.js +516 -0
  173. package/src/lib/cowork-mcp-tools.js +47 -2
  174. package/src/lib/cowork-task-runner.js +11 -3
  175. package/src/lib/doctor-checkup.js +9 -7
  176. package/src/lib/learning/skill-improver.js +21 -2
  177. package/src/lib/mcp-call-ledger-store.js +116 -3
  178. package/src/lib/mcp-call-ledger.js +59 -14
  179. package/src/lib/mcp-client.js +1 -0
  180. package/src/lib/mcp-stdio-environment.js +48 -0
  181. package/src/lib/mcp-stdio-executable-identity.js +1133 -0
  182. package/src/lib/mcp-stdio-execution-authority.js +363 -0
  183. package/src/lib/mcp-stdio-package-materialization.js +1424 -0
  184. package/src/lib/mcp-tool-metadata.js +436 -0
  185. package/src/lib/mcp-tool-result.js +367 -0
  186. package/src/lib/observability/index.js +71 -0
  187. package/src/lib/packer/native-update-state.js +979 -1
  188. package/src/lib/packer/pack-update-applier.js +1050 -125
  189. package/src/lib/packer/pkg-config-generator.js +1 -0
  190. package/src/lib/paths.js +53 -0
  191. package/src/lib/pr-link-ledger.js +8 -27
  192. package/src/lib/pr-link-store.js +26 -0
  193. package/src/lib/process-execution-broker/index.js +192 -35
  194. package/src/lib/process-execution-broker/platform-sandbox.js +291 -24
  195. package/src/lib/process-tree-termination.js +253 -0
  196. package/src/lib/recent-session.js +17 -17
  197. package/src/lib/secure-file-identity.js +47 -0
  198. package/src/lib/secure-fs.js +21 -2
  199. package/src/lib/session-anti-rollback-anchor.js +766 -0
  200. package/src/lib/session-host-lease.js +492 -0
  201. package/src/lib/session-host-snapshot.js +9 -5
  202. package/src/lib/session-persistence-failure.js +109 -0
  203. package/src/lib/session-picker.js +26 -3
  204. package/src/lib/session-projection.js +5 -1
  205. package/src/lib/session-search.js +25 -11
  206. package/src/lib/session-tail.js +31 -1
  207. package/src/lib/side-effect-ledger-store.js +2 -2
  208. package/src/lib/skill-budget.js +222 -0
  209. package/src/lib/skill-execution-authority.js +385 -0
  210. package/src/lib/skill-execution-identity.js +39 -2
  211. package/src/lib/skill-loader.js +322 -86
  212. package/src/lib/skill-mcp.js +43 -1
  213. package/src/lib/turn-binding-store.js +2 -2
  214. package/src/repl/agent-repl.js +483 -77
  215. package/src/repl/bg-dashboard.js +81 -28
  216. package/src/repl/chat-repl.js +77 -12
  217. package/src/runtime/agent-core.js +428 -113
  218. package/src/runtime/agent-runtime.js +26 -3
  219. package/src/runtime/cleanup-deadline.js +143 -0
  220. package/src/runtime/headless-runner.js +494 -149
  221. package/src/runtime/headless-stream.js +504 -75
  222. package/src/runtime/mcp-config.js +157 -10
  223. package/src/runtime/mcp-tool-search.js +0 -0
  224. package/src/runtime/output-backpressure.js +419 -0
  225. package/src/runtime/pipe-safety.js +56 -21
  226. package/src/assets/web-panel/assets/ChatBubbleRenderer-BUUyJ5QO.js +0 -1
  227. package/src/assets/web-panel/assets/MarkdownRenderer-1mNozZ8v.js +0 -1
  228. package/src/assets/web-panel/assets/MobileProjects-DqnoFPgR.js +0 -1
  229. package/src/assets/web-panel/assets/Providers-uO-10aP1.js +0 -1
  230. package/src/assets/web-panel/assets/devWarning-DosMaL-k.js +0 -1
  231. package/src/assets/web-panel/assets/index-0O5ePnUq.js +0 -1
  232. package/src/assets/web-panel/assets/index-ChAzPigF.js +0 -1
  233. package/src/lib/skill-process-broker.js +0 -85
@@ -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
+ }
@@ -14,7 +14,7 @@ import {
14
14
  projectWsTurnMessages,
15
15
  readVerifiedEvents as storeReadVerifiedEvents,
16
16
  readVerifiedProjection as storeReadVerifiedProjection,
17
- sessionExists as storeSessionExists,
17
+ sessionHasPersistedEvidence as storeSessionHasPersistedEvidence,
18
18
  } from "../harness/jsonl-session-store.js";
19
19
  import {
20
20
  createMcpLedgerEventReducer,
@@ -344,12 +344,16 @@ function createStreamingSessionHostProjection(sessionId) {
344
344
  /**
345
345
  * Read one real JSONL session for a host resume/attach boundary.
346
346
  *
347
- * `null` means no JSONL session exists and callers may use their legacy store.
348
- * A present but damaged/unanchored transcript returns a fail-closed snapshot
349
- * and no raw messages, so a host cannot silently fall back to stale history.
347
+ * `null` means no canonical JSONL evidence exists and callers may use their
348
+ * legacy store. A tombstoned, missing, damaged, or unanchored transcript
349
+ * returns a fail-closed snapshot and no raw messages, so a host cannot silently
350
+ * fall back to stale history or implicitly recreate a deleted generation.
350
351
  */
351
352
  export function readSessionHostResumeState(sessionId, dependencies = {}) {
352
- const exists = dependencies.sessionExists || storeSessionExists;
353
+ const exists =
354
+ dependencies.sessionHasPersistedEvidence ||
355
+ dependencies.sessionExists ||
356
+ storeSessionHasPersistedEvidence;
353
357
  const hasCustomLegacyReader =
354
358
  typeof dependencies.readVerifiedEvents === "function" &&
355
359
  dependencies.readVerifiedEvents !== storeReadVerifiedEvents;
@@ -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
  }
@@ -260,7 +260,11 @@ function actionPreviews(kind, state, source, sourceId, linkedSessionId) {
260
260
  );
261
261
  }
262
262
  if (active && interactive) {
263
- routes.reply = preview("terminal", ["attach", sourceId]);
263
+ routes.reply = preview(
264
+ "cli",
265
+ ["daemon", "reply", sourceId, PROJECTION_PROMPT_PLACEHOLDER, "--json"],
266
+ { mutates: true, input: "prompt" },
267
+ );
264
268
  routes.attach = preview("terminal", ["attach", sourceId]);
265
269
  }
266
270
  if (active) {