chainlesschain 0.162.163 → 0.162.165

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 (220) hide show
  1. package/package.json +3 -1
  2. package/src/assets/web-panel/assets/{AIOps-BQ0kgfA1.js → AIOps-DgGZr3PR.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Dr1MDANe.js → ActionButton-BAOgBIc_.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CZ5vsaXW.js → Analytics-Ds4GAvNF.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-BIBga881.js → AppLayout-CoJvCkoF.js} +4 -4
  6. package/src/assets/web-panel/assets/{Artifacts-vpXHRU3u.js → Artifacts-6EgUaE1T.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-B8exuoub.js → Audit-BYie_i74.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-923evRRN.js → BackgroundAgents-D5gxOhjd.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-sr_sswlQ.js → Backup-CIsVK2pT.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CnW-4XKT.js → BaseInput-ClorP55M.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-FZCsTZIL.js → Chat-CnhTPGGA.js} +6 -6
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-Bl1VXgwp.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-DFrxB59H.js → Checkbox-BRavl7TP.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-_b6WEel8.js → Codegen-BXg_ErNT.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CkbnnZ-K.js → Col-CfDPzFPq.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-BxM_82zW.js → Community-Cgo0httP.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-DYinP45R.js → Compact-DU5JcEQM.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-DyXp3sSP.js → Compliance-DCH_TVBl.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BEr2DhW7.js → Cowork-IGb5O2IH.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-B-Vwo1hN.js → Cron-CPp_Xp9F.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DbLxKddd.js → Crosschain-Cwrqklzw.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BsQ_LFw9.js → DID-D55sBQwM.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-lVlXQHu5.js → Dashboard-CedXhGmP.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-Beo_foO7.js → Dropdown-Dm6_v3V4.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DszHIXED.js → EmailListRenderer-CWjnuE3G.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DcAwoh81.js → FamilyGuardDashboard-DubTh7y-.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CuJLa2eb.js → Federation-DlSM4uJv.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cxd1lxhc.js → FormItemContext-BYhg0vtG.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BBvq5Jsw.js → GenericCardRenderer-Bkh7qjfK.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-WqPKetx6.js → Git-DSLWPKvn.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CWYtY3ke.js → Governance-DIj9O8BB.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-BnX1iiNV.js → Inference-Dhep3e5Y.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-BJuM8GN5.js → KnowledgeGraph-Cj8vzxB_.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DqLMNntm.js → Logs-Ba4SsPja.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-DUjyuVB3.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-_vGfmhNl.js → Marketplace-CGs9IIbf.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BeJDOkDj.js → McpTools-BTPAoX4K.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-Da0EEZDO.js → Memory-BJ6ISf2c.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-DSuzcCCy.js → MobileBridge-Ca6n3Dcn.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-D0RAk4e8.js → MobileProjects-lvilHuLt.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-BE9YgdA0.js → Mtc-CMo0acBV.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-fC2BpO5S.js → MtcAudit-Hz5wBvFX.js} +4 -4
  43. package/src/assets/web-panel/assets/{Multisig-D24Xxqw3.js → Multisig-BK2aKWE6.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-Bs3oGvS8.js → NLProgramming-CExpnYMI.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-BFjmAA-3.js → Notes-B7IFbuBo.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BX_9Bhv0.js → NotificationSettings-VrGchtb0.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-wjkZwJXI.js → OrderTableRenderer-CyrNTxkU.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-DBcdsVEP.js → Organization-I0C17GBP.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-DDyaF9vC.js → Overflow-CXcWvERe.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BB78L4he.js → P2P-FFSCXrZq.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-DqTiZFum.js → PdhVaultBrowser-1PB-p8Zk.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-INbnWv4j.js → Permissions-C1YHejsb.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BST-VmTq.js → PersonalDataHub-CEMPpgf8.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-CE7_tS2k.js → Pipeline-FoIScdLr.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-hK5L8dOB.js → Privacy-DDfbEzEj.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-BvkkUoDn.js → ProjectInit-Dso4kB0O.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-u7_EUtkm.js → ProjectSettings-WvDoydDc.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-5Fmt_8Nw.js → Projects-NJp0kzkd.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-B9eanarR.js → Providers-BuJXnOLI.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-C0dX5N2f.js → QrScannerModal-Bb3lMvGG.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CKTpG-PZ.js → QuickAsk-Ddxra3fK.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-Px_w_XQM.js → Recommend-BW-dR9qw.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-Di0fnYkM.js → RemoteSession-Hnm7-Qkm.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-vesnWjXt.js → Reputation-DKz6meHE.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-exbY6LkT.js → Row-BuAsoNTS.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B3O3x70t.js → RssFeed-_do9DMIH.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-uRA0DhNz.js → Search-CMlcw7ND.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CViqdH6K.js → Security-DhAkfSpM.js} +3 -3
  69. package/src/assets/web-panel/assets/{Services-CkXS97WN.js → Services-5zKLhy46.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-DxW5WYnE.js → Skeleton-DCMZ3wfv.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CLxmGFse.js → Skills-DxQgYCC_.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-Cr5fziXq.js → Sla-CJmE4cSN.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-H0JSb3cp.js → SpeechSettings-BgpRTPQp.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-DMYWMae7.js → SyncSettings-DkogtYbB.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CbYyDdF4.js → Tasks-D5OrwAl9.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Cpc8JjiK.js → Templates-DAZIu5nN.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-CRh77Jda.js → Tenant-BRJMI-su.js} +1 -1
  78. package/src/assets/web-panel/assets/Terminal-CMXLqQKF.js +3 -0
  79. package/src/assets/web-panel/assets/{TimelineRenderer-D41jzzbG.js → TimelineRenderer-D4z9aSGD.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C_jWEy6b.js → Tokens-zgYA4UOI.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-Dk1ZUhsy.js → Trigger-DMOgHX4m.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLVy4l7m.js → Trust-CEtoNwcT.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-C-FeqFDj.js → UkeySign-DFx2pQfO.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-CqVZzyMm.js → VideoEditing-DLssy-7B.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DX9BW9pu.js → Wallet-DQHc6gTl.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-7hQDpWw_.js → WebAuthn-DgKcODkn.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-6XqUAO0j.js → WorkflowEditor-cPEWfMOf.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-5R_vkaJG.js → chat-Coj7rSO1.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-huUPZdC-.js → colors-DiyGfr-6.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-CfJlgisE.js → compact-item-DB63aMXh.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-jO8heofr.js → createContext-CENpsYHg.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-XvRaIiyt.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DY8Vxqx7.js → hasIn-B9NOmk0K.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B300RPnT.js → index-76GMjcbq.js} +3 -3
  95. package/src/assets/web-panel/assets/{index-CA6sO5VD.js → index-7IMjHwGU.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-D1VLuww0.js → index-B0C6zl0y.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D0pet5Uy.js → index-B45osqnZ.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BHE8OIls.js → index-BXzMKVLp.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-vXs3B8GJ.js → index-BkoqtJzV.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-d1xkbtNz.js → index-BpmTl8QC.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BfCx4pFT.js → index-BramLq29.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-p85YaaKw.js → index-BvQq6gX2.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BsBtRWbA.js → index-Bvj07Ab1.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-KRHroryI.js → index-BxvNtndl.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BmqGerpC.js → index-C9d8hdgY.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CA90TZxG.js → index-CAXouogg.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Bxax9vQ6.js → index-CKNt2vfs.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BYLBPpt8.js → index-CRhEJ8JO.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DXBcvUnf.js → index-CTuX_MU0.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Cjxbu68P.js → index-CXRknr9W.js} +1 -1
  111. package/src/assets/web-panel/assets/index-CbO1zSXV.js +1 -0
  112. package/src/assets/web-panel/assets/{index-Cbe9G0V4.js → index-ClEy1v1o.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CRElRhyS.js → index-CmNCDN5t.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-v3pTaEQm.js → index-CvTJ-x6b.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-ChvHubX3.js → index-CwkK9Kl2.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CLhBh-qE.js → index-CxVfbuj9.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BQS-uPOK.js → index-DAMKhjtT.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-C2iVSFpy.js → index-DANoIqXP.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-B74OXFxC.js → index-D_rZCxxI.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-dKx57io7.js → index-Dg0v-CXZ.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BRXOP09X.js → index-DqLC0iE4.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BtEXSoPL.js → index-DsPDEHOg.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DoI2sZm7.js → index-KDCuTzdx.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BAON7TzU.js → index-Kp2AETEw.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Cis_Ip7H.js → index-U_snupDl.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-j3O2EEJe.js → index-VDFbduzG.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BliaT02i.js → index-Y_UKQpsV.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-A48PtXw9.js → index-Yb9EfUnY.js} +1 -1
  129. package/src/assets/web-panel/assets/index-aCu2gbUb.js +1 -0
  130. package/src/assets/web-panel/assets/{index-CzKmteNS.js → index-mTK_ZHhE.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BCqxyvqp.js → index-nio98wGC.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-Db1cLWir.js → index-t6hXF2Cw.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-MCB7-raZ.js → initDefaultProps-GTwJVIOz.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-UWDF9swV.js → motion-BJQLsPx8.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-CsTmgXZU.js → move-DVQM0bdG.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-fQy6KGLg.js → mtc-parser-CMXI1I2y.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-woPnEjd1.js → omit-CAc8MDw9.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-Bs2dZ1gf.js → pickAttrs-BgqzoyP7.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-C7EVgPkg.js → placementArrow-B9ebCdRy.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-BH2kYCTN.js → responsiveObserve-Dy8D0D3n.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-BFBP2V_e.js → slide-Bcrc8J39.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-xdDxDYse.js → statusUtils-CDW6FOlc.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-aAWgPPQf.js → styleChecker-CWhJrDON.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DkGSAWoO.js → useFlexGapSupport-C82c33XI.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-BrOih40N.js → useFs-A6m--mih.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-eIYrgMqO.js → usePersonalDataHub-m-1laJY5.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-B8lutNSE.js → vnode-CL9oKKRa.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-CffNre7V.js → zoom-DrOyaof3.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +71 -14
  151. package/src/commands/agent.js +14 -0
  152. package/src/commands/doctor.js +85 -1
  153. package/src/commands/plugin.js +226 -9
  154. package/src/commands/review.js +17 -1
  155. package/src/commands/session.js +191 -0
  156. package/src/commands/team.js +19 -1
  157. package/src/data/changelog.json +10 -4
  158. package/src/gateways/ws/session-protocol.js +50 -0
  159. package/src/gateways/ws/ws-agent-handler.js +50 -0
  160. package/src/harness/jsonl-session-store.js +60 -0
  161. package/src/harness/worktree-isolator.js +140 -5
  162. package/src/lib/agent-schedule-store.js +30 -1
  163. package/src/lib/agent-team/team-worktree.js +37 -2
  164. package/src/lib/agent-tool-admission.js +125 -0
  165. package/src/lib/app-preview.js +360 -0
  166. package/src/lib/async-hook-supervisor.cjs +54 -0
  167. package/src/lib/capability-manifest.js +319 -0
  168. package/src/lib/capability-negotiation.js +23 -12
  169. package/src/lib/diagnostic-bundle.js +230 -0
  170. package/src/lib/doctor-checkup.js +223 -4
  171. package/src/lib/execution-location.js +286 -0
  172. package/src/lib/headless-manifest.js +13 -40
  173. package/src/lib/hook-stats-store.cjs +197 -0
  174. package/src/lib/idempotency.js +109 -0
  175. package/src/lib/jsonl-session-store.js +1 -0
  176. package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
  177. package/src/lib/lsp/lsp-manager.js +56 -0
  178. package/src/lib/monitor-event.js +290 -0
  179. package/src/lib/operation-fingerprint.js +217 -0
  180. package/src/lib/plugin-runtime/bin.js +37 -0
  181. package/src/lib/plugin-runtime/capabilities.js +19 -0
  182. package/src/lib/plugin-runtime/capability-consent.js +28 -0
  183. package/src/lib/plugin-runtime/hooks.js +37 -0
  184. package/src/lib/plugin-runtime/lsp.js +38 -0
  185. package/src/lib/plugin-runtime/mcp.js +40 -0
  186. package/src/lib/plugin-runtime/monitors.js +37 -0
  187. package/src/lib/plugin-runtime/plugin-options.js +151 -0
  188. package/src/lib/pr-automation-policy.js +224 -0
  189. package/src/lib/project-instructions.js +9 -1
  190. package/src/lib/remote-approval-bridge.js +66 -13
  191. package/src/lib/repl-rewind.js +101 -0
  192. package/src/lib/review-comment-anchor.js +241 -0
  193. package/src/lib/review-pipeline.js +46 -7
  194. package/src/lib/runtime-checkup.js +221 -0
  195. package/src/lib/secret-scan.js +409 -0
  196. package/src/lib/session-branch.js +0 -0
  197. package/src/lib/side-effect-ledger.js +105 -0
  198. package/src/lib/skill-loader.js +32 -0
  199. package/src/lib/skill-path-scope.js +63 -0
  200. package/src/lib/sub-agent-context.js +36 -4
  201. package/src/lib/sub-agent-registry.js +19 -0
  202. package/src/lib/telemetry/span-recorder.js +17 -4
  203. package/src/lib/terminal-context.js +195 -0
  204. package/src/lib/unattended-action-policy.js +182 -0
  205. package/src/lib/worktree-cleanup-safety.js +138 -0
  206. package/src/lib/worktree-isolator.js +8 -0
  207. package/src/lib/worktree-sparse.js +131 -0
  208. package/src/repl/agent-repl.js +169 -40
  209. package/src/runtime/agent-core.js +220 -24
  210. package/src/runtime/diagnostics.js +60 -0
  211. package/src/runtime/headless-runner.js +135 -2
  212. package/src/runtime/headless-stream.js +10 -1
  213. package/src/runtime/policies/agent-policy.js +5 -0
  214. package/src/runtime/system-prompt.js +21 -8
  215. package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +0 -1
  216. package/src/assets/web-panel/assets/MarkdownRenderer-BqYwEYou.js +0 -1
  217. package/src/assets/web-panel/assets/Terminal-4D8tNBO5.js +0 -3
  218. package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +0 -1
  219. package/src/assets/web-panel/assets/index-BxUKMRpP.js +0 -1
  220. package/src/assets/web-panel/assets/index-DscU9iPI.js +0 -1
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * hook-stats-store — a small, best-effort, cross-session RELIABILITY aggregate
5
+ * for async settings hooks, so `cc doctor` (a separate process) can surface
6
+ * hooks that have been slow, repeatedly failing, or circuit-broken. The gap's
7
+ * P2 "Doctor" wants "慢/熔断 Hook" health, but that state otherwise lives only
8
+ * in-memory in AsyncHookSupervisor for the life of one process.
9
+ *
10
+ * Design: the supervisor folds each completed hook run into an in-memory
11
+ * aggregate (bounded by the number of DISTINCT hooks, not runs) and persists it
12
+ * once on stopAll() — NOT per run — so there is no write amplification on the
13
+ * hook hot path. Persistence merges with whatever is already on disk, so counts
14
+ * accumulate across sessions. Reads/writes are best-effort (a corrupt/ío-failed
15
+ * file never blocks a hook or the agent).
16
+ *
17
+ * Pure aggregation (no clock/RNG/fs in the fold) so it's fully unit-testable;
18
+ * load/save take an injected fs. The default circuit threshold mirrors the
19
+ * doctor evaluator's `hookFailureThreshold`.
20
+ */
21
+
22
+ const os = require("node:os");
23
+ const path = require("node:path");
24
+ const fsDefault = require("node:fs");
25
+
26
+ const MAX_HOOKS = 200; // bound distinct-hook entries; drop least-recently-run
27
+ const DEFAULT_CIRCUIT_THRESHOLD = 3;
28
+ const CONFIG_DIR_NAME = ".chainlesschain";
29
+
30
+ /** Stable per-hook id from its (event, command). */
31
+ function hookKey(command, event) {
32
+ return `${event || "*"}::${String(command || "").trim()}`;
33
+ }
34
+
35
+ /** Default on-disk location (matches the ESM getHomeDir() layout). */
36
+ function defaultHookStatsPath() {
37
+ return path.join(os.homedir(), CONFIG_DIR_NAME, "hook-stats.json");
38
+ }
39
+
40
+ /** Fresh empty aggregate map. */
41
+ function emptyStats() {
42
+ return {};
43
+ }
44
+
45
+ /**
46
+ * Fold ONE completed run into the aggregate (mutates + returns `stats`). Pure:
47
+ * `now`/`ok`/`ms` are supplied by the caller. `consecutiveFailures` resets on
48
+ * any success — the circuit signal is "N failures in a row", not lifetime.
49
+ */
50
+ function aggregateRun(stats, { command, event, ok, ms, now }) {
51
+ if (!stats || typeof stats !== "object") stats = {};
52
+ const id = hookKey(command, event);
53
+ const dur = Number.isFinite(Number(ms)) ? Number(ms) : 0;
54
+ const at = Number.isFinite(Number(now)) ? Number(now) : 0;
55
+ const e = stats[id] || {
56
+ id,
57
+ command: command || "",
58
+ event: event || null,
59
+ runs: 0,
60
+ failures: 0,
61
+ consecutiveFailures: 0,
62
+ lastFailureAt: null,
63
+ totalMs: 0,
64
+ maxMs: 0,
65
+ lastRunAt: 0,
66
+ };
67
+ e.runs += 1;
68
+ e.totalMs += dur;
69
+ if (dur > e.maxMs) e.maxMs = dur;
70
+ if (at > e.lastRunAt) e.lastRunAt = at;
71
+ if (ok === true) {
72
+ e.consecutiveFailures = 0;
73
+ } else {
74
+ e.failures += 1;
75
+ e.consecutiveFailures += 1;
76
+ e.lastFailureAt = at;
77
+ }
78
+ stats[id] = e;
79
+ boundStats(stats);
80
+ return stats;
81
+ }
82
+
83
+ /** Keep only the MAX_HOOKS most-recently-run entries. */
84
+ function boundStats(stats) {
85
+ const keys = Object.keys(stats);
86
+ if (keys.length <= MAX_HOOKS) return stats;
87
+ keys
88
+ .sort((a, b) => (stats[a].lastRunAt || 0) - (stats[b].lastRunAt || 0))
89
+ .slice(0, keys.length - MAX_HOOKS)
90
+ .forEach((k) => delete stats[k]);
91
+ return stats;
92
+ }
93
+
94
+ /**
95
+ * Merge a session `delta` aggregate INTO a `base` aggregate (returns a new map).
96
+ * Counts add; maxMs/lastRunAt/lastFailureAt take the larger; the session's
97
+ * consecutiveFailures wins (it reflects the most recent streak).
98
+ */
99
+ function mergeStats(base, delta) {
100
+ const out = {};
101
+ for (const src of [base, delta]) {
102
+ if (!src || typeof src !== "object") continue;
103
+ for (const [id, e] of Object.entries(src)) {
104
+ if (!e || typeof e !== "object") continue;
105
+ const cur = out[id];
106
+ if (!cur) {
107
+ out[id] = { ...e };
108
+ continue;
109
+ }
110
+ cur.runs += e.runs || 0;
111
+ cur.failures += e.failures || 0;
112
+ cur.totalMs += e.totalMs || 0;
113
+ cur.maxMs = Math.max(cur.maxMs || 0, e.maxMs || 0);
114
+ cur.lastRunAt = Math.max(cur.lastRunAt || 0, e.lastRunAt || 0);
115
+ cur.lastFailureAt =
116
+ Math.max(cur.lastFailureAt || 0, e.lastFailureAt || 0) || null;
117
+ // `delta` is applied second, so its streak (most recent) wins.
118
+ cur.consecutiveFailures =
119
+ e.consecutiveFailures ?? cur.consecutiveFailures;
120
+ cur.command = e.command || cur.command;
121
+ cur.event = e.event ?? cur.event;
122
+ }
123
+ }
124
+ return boundStats(out);
125
+ }
126
+
127
+ /**
128
+ * Shape the aggregate for the doctor `checkHooks` evaluator: id, lifetime
129
+ * failure count, average duration, and a derived circuit-open flag.
130
+ */
131
+ function toCheckHooksInput(
132
+ stats,
133
+ { circuitThreshold = DEFAULT_CIRCUIT_THRESHOLD } = {},
134
+ ) {
135
+ if (!stats || typeof stats !== "object") return [];
136
+ return Object.values(stats).map((e) => ({
137
+ id: e.command || e.id,
138
+ event: e.event || null,
139
+ failures: e.failures || 0,
140
+ avgMs: e.runs > 0 ? e.totalMs / e.runs : 0,
141
+ circuitOpen: (e.consecutiveFailures || 0) >= circuitThreshold,
142
+ }));
143
+ }
144
+
145
+ /** Best-effort read of the on-disk aggregate (→ {} on missing/corrupt). */
146
+ function loadHookStats(file = defaultHookStatsPath(), fs = fsDefault) {
147
+ try {
148
+ if (!fs.existsSync(file)) return {};
149
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
150
+ return parsed && typeof parsed === "object" ? parsed : {};
151
+ } catch {
152
+ return {};
153
+ }
154
+ }
155
+
156
+ /** Best-effort atomic write (tmp + rename). Never throws. */
157
+ function saveHookStats(stats, file = defaultHookStatsPath(), fs = fsDefault) {
158
+ try {
159
+ const dir = path.dirname(file);
160
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
161
+ const tmp = `${file}.tmp`;
162
+ fs.writeFileSync(tmp, JSON.stringify(stats), "utf-8");
163
+ fs.renameSync(tmp, file);
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Load the on-disk aggregate, fold in a session `delta`, and write it back.
172
+ * Best-effort end-to-end — the single call a supervisor makes on stopAll().
173
+ */
174
+ function persistSessionStats(
175
+ delta,
176
+ file = defaultHookStatsPath(),
177
+ fs = fsDefault,
178
+ ) {
179
+ if (!delta || Object.keys(delta).length === 0) return false;
180
+ const merged = mergeStats(loadHookStats(file, fs), delta);
181
+ return saveHookStats(merged, file, fs);
182
+ }
183
+
184
+ module.exports = {
185
+ hookKey,
186
+ defaultHookStatsPath,
187
+ emptyStats,
188
+ aggregateRun,
189
+ mergeStats,
190
+ boundStats,
191
+ toCheckHooksInput,
192
+ loadHookStats,
193
+ saveHookStats,
194
+ persistSessionStats,
195
+ MAX_HOOKS,
196
+ DEFAULT_CIRCUIT_THRESHOLD,
197
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Content-addressed idempotency — the P0-2 "Diff Apply 使用内容哈希/版本号" +
3
+ * "外部 API 优先使用 Provider Idempotency Key" primitives.
4
+ *
5
+ * All PURE (deterministic, no clock / RNG / I/O):
6
+ *
7
+ * - `classifyEditReplay` — decide, from the CURRENT file content alone, whether
8
+ * a literal edit still applies, has ALREADY landed (a resumed replay), or
9
+ * genuinely conflicts. Lets a recovered worker skip an edit it already made
10
+ * instead of double-applying it or erroring confusingly.
11
+ * - `editIdempotencyKey` / `operationIdempotencyKey` — stable keys derived from
12
+ * the operation and a canonicalized (order-independent) view of its args, so
13
+ * a retry of the SAME effect yields the SAME key. An external provider that
14
+ * honors `Idempotency-Key` then de-dupes a resumed replay; internally the key
15
+ * tags each side-effect ledger op for duplicate detection.
16
+ */
17
+
18
+ import { createHash } from "node:crypto";
19
+
20
+ function sha256Hex(input) {
21
+ return createHash("sha256").update(String(input), "utf8").digest("hex");
22
+ }
23
+
24
+ /**
25
+ * Canonicalize a value so semantically-equal args hash identically regardless of
26
+ * key order. Arrays keep order (order is meaningful); object keys are sorted.
27
+ */
28
+ function canonicalize(v) {
29
+ if (v == null) return null;
30
+ if (Array.isArray(v)) return v.map(canonicalize);
31
+ if (typeof v === "object") {
32
+ const out = {};
33
+ for (const k of Object.keys(v).sort()) out[k] = canonicalize(v[k]);
34
+ return out;
35
+ }
36
+ return v;
37
+ }
38
+
39
+ /** Fate of an edit checked against the current on-disk content. */
40
+ export const EDIT_REPLAY = Object.freeze({
41
+ APPLY: "apply", // old_string present — the edit still applies
42
+ ALREADY_APPLIED: "already_applied", // old gone, new present — idempotent no-op
43
+ CONFLICT: "conflict", // neither present — a genuine mismatch, surface it
44
+ });
45
+
46
+ /**
47
+ * Decide the fate of a literal (old→new) edit against CURRENT content — the
48
+ * content-addressed idempotency check. On a resumed replay the edit already
49
+ * landed, so `old_string` is gone and `new_string` is present → already_applied
50
+ * (skip, never double-apply). PURE.
51
+ *
52
+ * Deletions (`new_string === ""`) are deliberately NOT auto-detected as
53
+ * already-applied: "old absent + nothing to look for" is indistinguishable from
54
+ * "old never existed", so those keep surfacing as a conflict (the safe default).
55
+ *
56
+ * @param {{content:string, oldString:string, newString:string, replaceAll?:boolean}} p
57
+ * @returns {"apply"|"already_applied"|"conflict"}
58
+ */
59
+ export function classifyEditReplay({ content, oldString, newString }) {
60
+ const text = String(content ?? "");
61
+ const oldS = String(oldString ?? "");
62
+ const newS = String(newString ?? "");
63
+ if (oldS && text.includes(oldS)) return EDIT_REPLAY.APPLY;
64
+ // old_string is absent. A present, non-empty new_string means the intended
65
+ // change is already in the file — the edit already happened.
66
+ if (newS && text.includes(newS)) return EDIT_REPLAY.ALREADY_APPLIED;
67
+ return EDIT_REPLAY.CONFLICT;
68
+ }
69
+
70
+ /** Stable idempotency key for a literal edit against a target file. */
71
+ export function editIdempotencyKey({ path, oldString, newString, replaceAll }) {
72
+ return (
73
+ "edit_" +
74
+ sha256Hex(
75
+ JSON.stringify([
76
+ String(path || ""),
77
+ String(oldString ?? ""),
78
+ String(newString ?? ""),
79
+ replaceAll === true,
80
+ ]),
81
+ ).slice(0, 40)
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Deterministic idempotency key for an external, irreversible operation — stable
87
+ * across retries so a provider honoring `Idempotency-Key` de-dupes a resumed
88
+ * replay, and so the side-effect ledger can spot a duplicated commit. Derived
89
+ * from the tool + a canonicalized view of its args; NO wall-clock / RNG so the
90
+ * SAME logical effect always maps to the SAME key.
91
+ *
92
+ * `scope` is optional namespacing (e.g. a session id); OMIT it when you want a
93
+ * pure retry of the same call to collide with its earlier attempt.
94
+ *
95
+ * @param {{tool:string, args?:object, scope?:string}} p
96
+ * @returns {string}
97
+ */
98
+ export function operationIdempotencyKey({ tool, args, scope } = {}) {
99
+ return (
100
+ "op_" +
101
+ sha256Hex(
102
+ JSON.stringify([
103
+ String(tool || ""),
104
+ scope == null ? "" : String(scope),
105
+ canonicalize(args ?? null),
106
+ ]),
107
+ ).slice(0, 40)
108
+ );
109
+ }
@@ -24,6 +24,7 @@ export {
24
24
  renameSession,
25
25
  pruneJsonlSessions,
26
26
  forkSession,
27
+ createBranchSession,
27
28
  sessionExists,
28
29
  getLastSessionId,
29
30
  migrateLegacySessions,
@@ -257,6 +257,64 @@ describe("crash-loop restart guard", () => {
257
257
  expect(lastClient).not.toBe(quarantined);
258
258
  await mgr.disposeAll();
259
259
  });
260
+
261
+ describe("exponential restart backoff (opt-in)", () => {
262
+ it("computes exponential backoff per crash, capped, and 0 when disabled", () => {
263
+ const on = new LSPManager({
264
+ restartBackoffBaseMs: 1000,
265
+ restartBackoffMaxMs: 8000,
266
+ });
267
+ expect(on._restartBackoffMs(0)).toBe(0); // first start — no wait
268
+ expect(on._restartBackoffMs(1)).toBe(1000);
269
+ expect(on._restartBackoffMs(2)).toBe(2000);
270
+ expect(on._restartBackoffMs(3)).toBe(4000);
271
+ expect(on._restartBackoffMs(4)).toBe(8000);
272
+ expect(on._restartBackoffMs(5)).toBe(8000); // capped
273
+ // Disabled by default → always 0 (byte-identical immediate respawn).
274
+ const off = new LSPManager();
275
+ expect(off.restartBackoffBaseMs).toBe(0);
276
+ expect(off._restartBackoffMs(2)).toBe(0);
277
+ });
278
+
279
+ it("degrades (backing off) during the cooldown, then re-spawns once it elapses", async () => {
280
+ let t = 1000;
281
+ const mgr = new LSPManager({
282
+ maxRestarts: 5,
283
+ restartWindowMs: 60000,
284
+ restartBackoffBaseMs: 1000,
285
+ restartBackoffMaxMs: 8000,
286
+ now: () => t,
287
+ });
288
+ await mgr.ensureFor("/proj/src/a.ts"); // spawn #1
289
+ const first = lastClient;
290
+ crash(first); // 1 crash at t=1000
291
+ // Same tick → within the 1st-crash backoff (1000ms) → degrade, no respawn.
292
+ const backingOff = await mgr.ensureFor("/proj/src/a.ts");
293
+ expect(backingOff.unavailable).toBe(true);
294
+ expect(backingOff.backoff).toBe(true);
295
+ expect(backingOff.quarantined).toBeUndefined();
296
+ expect(backingOff.retryInMs).toBe(1000);
297
+ expect(lastClient).toBe(first); // did NOT spawn during cooldown
298
+ // Advance past the cooldown → a fresh spawn is allowed.
299
+ t += 1000;
300
+ const ready = await mgr.ensureFor("/proj/src/a.ts");
301
+ expect(ready.unavailable).toBeUndefined();
302
+ expect(lastClient).not.toBe(first);
303
+ await mgr.disposeAll();
304
+ });
305
+
306
+ it("does NOT back off by default — respawns a crashed server on the next request", async () => {
307
+ const mgr = new LSPManager({ maxRestarts: 3, now: () => 5000 });
308
+ await mgr.ensureFor("/proj/src/a.ts");
309
+ const first = lastClient;
310
+ crash(first);
311
+ // Same tick, backoff disabled → immediate fresh spawn (prior behaviour).
312
+ const ready = await mgr.ensureFor("/proj/src/a.ts");
313
+ expect(ready.unavailable).toBeUndefined();
314
+ expect(lastClient).not.toBe(first);
315
+ await mgr.disposeAll();
316
+ });
317
+ });
260
318
  });
261
319
 
262
320
  describe("diagnostics", () => {
@@ -33,6 +33,14 @@ export class LSPManager {
33
33
  * @param {number} [opts.restartWindowMs] sliding window for the crash count.
34
34
  * A server that stays healthy for this long has its crash history pruned and
35
35
  * can restart fresh. Default 30000.
36
+ * @param {number} [opts.restartBackoffBaseMs] exponential backoff BETWEEN
37
+ * restart attempts: after the Nth recent crash the (N+1)th re-spawn waits
38
+ * `base·2^(N-1)` (capped by `restartBackoffMaxMs`) before it is allowed,
39
+ * spreading out a fast startup-crash loop instead of bursting `maxRestarts`
40
+ * spawns back-to-back. Default 0 = OFF (byte-identical to the prior
41
+ * immediate-respawn behaviour); opt-in pending evaluation.
42
+ * @param {number} [opts.restartBackoffMaxMs] cap for the exponential backoff.
43
+ * Default 8000.
36
44
  * @param {() => number} [opts.now] injected clock (deterministic crash-loop tests)
37
45
  */
38
46
  constructor(opts = {}) {
@@ -65,6 +73,19 @@ export class LSPManager {
65
73
  this.restartWindowMs = Number.isFinite(opts.restartWindowMs)
66
74
  ? opts.restartWindowMs
67
75
  : 30000;
76
+ // Exponential backoff BETWEEN restart attempts (distinct from the quarantine
77
+ // cap above). Without it, a server that crashes on startup is re-spawned on
78
+ // the very next request — bursting `maxRestarts` spawns back-to-back in
79
+ // milliseconds before quarantine. When enabled, the (N+1)th spawn after N
80
+ // recent crashes waits `base·2^(N-1)` (capped), so a fast crash loop can't
81
+ // storm the process table. Default 0 = OFF → byte-identical to the prior
82
+ // immediate-respawn behaviour (opt-in pending evaluation).
83
+ this.restartBackoffBaseMs = Number.isFinite(opts.restartBackoffBaseMs)
84
+ ? Math.max(0, opts.restartBackoffBaseMs)
85
+ : 0;
86
+ this.restartBackoffMaxMs = Number.isFinite(opts.restartBackoffMaxMs)
87
+ ? opts.restartBackoffMaxMs
88
+ : 8000;
68
89
  this._now = typeof opts.now === "function" ? opts.now : () => Date.now();
69
90
  /** key → array of recent crash timestamps (survives entry re-spawn) */
70
91
  this._crashLog = new Map();
@@ -80,6 +101,18 @@ export class LSPManager {
80
101
  return pruned;
81
102
  }
82
103
 
104
+ /**
105
+ * Minimum delay before the next restart attempt given `crashCount` recent
106
+ * crashes: exponential `base·2^(crashCount-1)`, capped at `restartBackoffMaxMs`.
107
+ * Returns 0 when backoff is disabled (base 0) or `crashCount <= 0` (a first
108
+ * start). Deterministic (no jitter) so crash-loop tests stay reproducible.
109
+ */
110
+ _restartBackoffMs(crashCount) {
111
+ if (this.restartBackoffBaseMs <= 0 || crashCount <= 0) return 0;
112
+ const exp = this.restartBackoffBaseMs * 2 ** (crashCount - 1);
113
+ return Math.min(exp, this.restartBackoffMaxMs);
114
+ }
115
+
83
116
  /**
84
117
  * Ensure a server is running for `filePath` and the file is open. Returns
85
118
  * `{ client, uri, languageId }`, or `{ unavailable: true, reason }` when no
@@ -125,6 +158,29 @@ export class LSPManager {
125
158
  quarantined: true,
126
159
  };
127
160
  }
161
+ // Under the quarantine cap but recently crashed: when backoff is enabled,
162
+ // space out the re-spawn exponentially so a fast startup-crash loop doesn't
163
+ // storm the process table. The caller degrades to text search until the
164
+ // cooldown elapses — a transient state, distinct from quarantine.
165
+ if (crashes.length > 0) {
166
+ const backoffMs = this._restartBackoffMs(crashes.length);
167
+ if (backoffMs > 0) {
168
+ const lastCrash = crashes[crashes.length - 1];
169
+ const sinceLast = this._now() - lastCrash;
170
+ if (sinceLast < backoffMs) {
171
+ const retryInMs = backoffMs - sinceLast;
172
+ return {
173
+ unavailable: true,
174
+ reason:
175
+ `${resolved.id} restarting after a crash — backing off ` +
176
+ `~${Math.max(0, Math.round(retryInMs / 1000))}s ` +
177
+ `(attempt ${crashes.length + 1}/${this.maxRestarts})`,
178
+ backoff: true,
179
+ retryInMs,
180
+ };
181
+ }
182
+ }
183
+ }
128
184
  entry = await this._getOrStartServer(key, resolved, projectRoot);
129
185
  }
130
186