chainlesschain 0.162.161 → 0.162.163

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 (209) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-DMn528q-.js → AIOps-BQ0kgfA1.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-D9ZcDCJL.js → ActionButton-Dr1MDANe.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-A_K-8PX0.js → Analytics-CZ5vsaXW.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-D4pFW3X6.js → AppLayout-BIBga881.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-Dz46COAd.js → Artifacts-vpXHRU3u.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-cbMjbyg0.js → Audit-B8exuoub.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-Dtf88GAj.js → BackgroundAgents-923evRRN.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-C5KRxn52.js → Backup-sr_sswlQ.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-Dd2UK3BN.js → BaseInput-CnW-4XKT.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-lptKTFdG.js → Chat-FZCsTZIL.js} +5 -5
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-_yspG6rG.js → Checkbox-DFrxB59H.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BGq_GAha.js → Codegen-_b6WEel8.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CePor71g.js → Col-CkbnnZ-K.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CLOWhIT7.js → Community-BxM_82zW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CivJt2vT.js → Compact-DYinP45R.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Btmo1GGE.js → Compliance-DyXp3sSP.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Ct35OlG4.js → Cowork-BEr2DhW7.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-CTc2GmhB.js → Cron-B-Vwo1hN.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-ZtiBETk1.js → Crosschain-DbLxKddd.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BNqMQQXw.js → DID-BsQ_LFw9.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-BmzZd-9V.js → Dashboard-lVlXQHu5.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CMVgvP2M.js → Dropdown-Beo_foO7.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-CcqQFAfT.js → EmailListRenderer-DszHIXED.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CTqB1_4V.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CiKOOzlb.js → Federation-CuJLa2eb.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Wk-OT0gr.js → FormItemContext-Cxd1lxhc.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DbEQT6ue.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-0ZvCPSD9.js → Git-WqPKetx6.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CmVuKkPb.js → Governance-CWYtY3ke.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CTWE3Jwg.js → Inference-BnX1iiNV.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-CBEIF00L.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DLhxDwVG.js → Logs-DqLMNntm.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-BqYwEYou.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-BgsCBrnc.js → Marketplace-_vGfmhNl.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-ClBUGDS2.js → McpTools-BeJDOkDj.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-CnG_ZnYw.js → Memory-Da0EEZDO.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-BUOAbR1Z.js → MobileBridge-DSuzcCCy.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-Dnt_9-WB.js → MobileProjects-D0RAk4e8.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-CLjt4kCD.js → Mtc-BE9YgdA0.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-f24U9Aes.js → MtcAudit-fC2BpO5S.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-GrmGQbSq.js → Multisig-D24Xxqw3.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-DADaWn-n.js → NLProgramming-Bs3oGvS8.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-xtf5RX6C.js → Notes-BFjmAA-3.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-Cd27Rgtx.js → NotificationSettings-BX_9Bhv0.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-fXRxlDjl.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-joCFlNB0.js → Organization-DBcdsVEP.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-B1HoURq0.js → Overflow-DDyaF9vC.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-DiwsKbT7.js → P2P-BB78L4he.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Xe-BsW6S.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-C23qi8kl.js → Permissions-INbnWv4j.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BS1syX2f.js → PersonalDataHub-BST-VmTq.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-DRYwwDyh.js → Pipeline-CE7_tS2k.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BQKaDHPH.js → Privacy-hK5L8dOB.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-BC6F9xiG.js → ProjectInit-BvkkUoDn.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-ClFPc1Nd.js → ProjectSettings-u7_EUtkm.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-5Fmt_8Nw.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-CLWuOUNt.js → Providers-B9eanarR.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-DiqULTxb.js → QrScannerModal-C0dX5N2f.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-9A03P-E_.js → QuickAsk-CKTpG-PZ.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CJZAnbq_.js → Recommend-Px_w_XQM.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C9A1R77h.js → RemoteSession-Di0fnYkM.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BIZc6Rky.js → Reputation-vesnWjXt.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-DfIPNtuG.js → Row-exbY6LkT.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-C5znFcQ1.js → RssFeed-B3O3x70t.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-DSs2z3OX.js → Search-uRA0DhNz.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CV6LcrLY.js → Security-CViqdH6K.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-cmhoX0Xj.js → Services-CkXS97WN.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-tCsg2po7.js → Skeleton-DxW5WYnE.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-BK1KUDiy.js → Skills-CLxmGFse.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-yRgyeA82.js → Sla-Cr5fziXq.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-c1mo1mlp.js → SpeechSettings-H0JSb3cp.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-koIAfuT3.js → SyncSettings-DMYWMae7.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-DkkreAaB.js → Tasks-CbYyDdF4.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-fy5Ctf9M.js → Templates-Cpc8JjiK.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-Bw70Jf6S.js → Tenant-CRh77Jda.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BKh6Rx-4.js → Terminal-4D8tNBO5.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-Dg54WP0c.js → TimelineRenderer-D41jzzbG.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-BvvD5SxJ.js → Tokens-C_jWEy6b.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-B7Ug9x4e.js → Trigger-Dk1ZUhsy.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-BjvuMORY.js → Trust-CLVy4l7m.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-CZqhkx8p.js → UkeySign-C-FeqFDj.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-ClR50lhX.js → VideoEditing-CqVZzyMm.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DlfppYjx.js → Wallet-DX9BW9pu.js} +2 -2
  86. package/src/assets/web-panel/assets/{WebAuthn-BirgVr8B.js → WebAuthn-7hQDpWw_.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-B8DP_bax.js → WorkflowEditor-6XqUAO0j.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-DA8AUZzL.js → chat-5R_vkaJG.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-C38pLGp1.js → colors-huUPZdC-.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-81rtr98o.js → compact-item-CfJlgisE.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-D34ixq8t.js → createContext-jO8heofr.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-D4bzqTrZ.js → hasIn-DY8Vxqx7.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-E1OJRXs4.js → index-A48PtXw9.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-D3zCCZRS.js → index-B300RPnT.js} +3 -3
  96. package/src/assets/web-panel/assets/{index-Cd3KuNK5.js → index-B74OXFxC.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dmo3dFHb.js → index-BAON7TzU.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Ddk0e_Pi.js → index-BCqxyvqp.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-9qoVpPt6.js → index-BHE8OIls.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DBFSo1NF.js → index-BQS-uPOK.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DUQLXLQM.js → index-BRXOP09X.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-D6GfvEQG.js → index-BYLBPpt8.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DFyWKOBD.js → index-BfCx4pFT.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BP-zkXK8.js → index-BliaT02i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DMgnRwrK.js → index-BmqGerpC.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BIP62a6r.js → index-BsBtRWbA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DkHqMnn3.js → index-BtEXSoPL.js} +1 -1
  108. package/src/assets/web-panel/assets/index-BxUKMRpP.js +1 -0
  109. package/src/assets/web-panel/assets/{index-B8mkLM4x.js → index-Bxax9vQ6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-ZRJRItm0.js → index-C2iVSFpy.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CP5alL4t.js → index-CA6sO5VD.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BS3M5H9V.js → index-CA90TZxG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DvfCEJG-.js → index-CLhBh-qE.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-tKiC8T-4.js → index-CRElRhyS.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B0QArPQt.js → index-Cbe9G0V4.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-COe_cOLJ.js → index-ChvHubX3.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BvL_3J_n.js → index-Cis_Ip7H.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-F76w1NiL.js → index-Cjxbu68P.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BlxEphl7.js → index-CzKmteNS.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CuS-UPoj.js → index-D0pet5Uy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CbhE3j5j.js → index-D1VLuww0.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D_2VYPr1.js → index-DXBcvUnf.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DakfFZ-k.js → index-Db1cLWir.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-fI5sXMLg.js → index-DoI2sZm7.js} +1 -1
  125. package/src/assets/web-panel/assets/index-DscU9iPI.js +1 -0
  126. package/src/assets/web-panel/assets/{index-CoVgZw_b.js → index-KRHroryI.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BvD7b2IK.js → index-d1xkbtNz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BtGxuQrO.js → index-dKx57io7.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BqIhLAbA.js → index-j3O2EEJe.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BXd2_LmH.js → index-p85YaaKw.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DFHa1HIg.js → index-v3pTaEQm.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D0Ih7Hwn.js → index-vXs3B8GJ.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-ByyvflIR.js → initDefaultProps-MCB7-raZ.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-B3FQm2By.js → motion-UWDF9swV.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-BSfns6Ef.js → move-CsTmgXZU.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-CTG1Ieri.js → mtc-parser-fQy6KGLg.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-DObM_2j6.js → omit-woPnEjd1.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-DOfelQ1t.js → pickAttrs-Bs2dZ1gf.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-CHZILds_.js → placementArrow-C7EVgPkg.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-REbucKSx.js → responsiveObserve-BH2kYCTN.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-C6EZzD7o.js → slide-BFBP2V_e.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BxkWS-g3.js → statusUtils-xdDxDYse.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-Chklyoq6.js → styleChecker-aAWgPPQf.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-CRVwKrrI.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DV24ik_J.js → useFs-BrOih40N.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-C7vIo4Pg.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-By-v5GlF.js → vnode-B8lutNSE.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-B1w9Am3r.js → zoom-CffNre7V.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +254 -23
  151. package/src/commands/agent.js +105 -11
  152. package/src/commands/context.js +125 -14
  153. package/src/commands/hook.js +181 -7
  154. package/src/commands/plugin.js +239 -1
  155. package/src/commands/review.js +352 -2
  156. package/src/data/changelog.json +14 -4
  157. package/src/gateways/ws/remote-session-protocol.js +39 -0
  158. package/src/gateways/ws/session-protocol.js +5 -2
  159. package/src/lib/agent-schedule-store.js +289 -10
  160. package/src/lib/agents.js +19 -0
  161. package/src/lib/background-agent-supervisor.js +34 -7
  162. package/src/lib/channels/channel-manager.js +54 -2
  163. package/src/lib/cli-context-engineering.js +9 -2
  164. package/src/lib/context-breakdown.js +116 -0
  165. package/src/lib/docs-drift.js +166 -0
  166. package/src/lib/doctor-checkup.js +128 -0
  167. package/src/lib/hook-event-bus.cjs +0 -0
  168. package/src/lib/hook-event-log.cjs +193 -0
  169. package/src/lib/interaction-adapter.js +34 -4
  170. package/src/lib/json-schema-output.js +75 -12
  171. package/src/lib/json-schema-validate.js +831 -0
  172. package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
  173. package/src/lib/lsp/diagnostics-scheduler.js +127 -0
  174. package/src/lib/operation-fingerprint.js +95 -0
  175. package/src/lib/plugin-runtime/capabilities.js +371 -0
  176. package/src/lib/plugin-runtime/capability-consent.js +175 -0
  177. package/src/lib/plugin-runtime/manifest.js +30 -0
  178. package/src/lib/plugin-runtime/policy.js +62 -0
  179. package/src/lib/plugin-runtime/scopes.js +20 -4
  180. package/src/lib/project-instructions.js +128 -0
  181. package/src/lib/remote-approval-bridge.js +16 -0
  182. package/src/lib/repl-goal.js +132 -0
  183. package/src/lib/repl-rewind.js +52 -0
  184. package/src/lib/review-pipeline.js +227 -0
  185. package/src/lib/runtime-checkup.js +318 -0
  186. package/src/lib/schedule-planner.js +17 -1
  187. package/src/lib/session-lifecycle.js +305 -0
  188. package/src/lib/settings-hook-events.cjs +52 -13
  189. package/src/lib/settings-loader.cjs +42 -0
  190. package/src/lib/side-effect-ledger-store.js +88 -0
  191. package/src/lib/side-effect-ledger.js +296 -0
  192. package/src/lib/sub-agent-context.js +74 -5
  193. package/src/lib/subagent-contract.js +383 -0
  194. package/src/lib/subagent-inheritance.js +112 -0
  195. package/src/lib/telemetry-ids.js +190 -0
  196. package/src/lib/turn-binding-store.js +119 -0
  197. package/src/lib/turn-binding.js +397 -0
  198. package/src/repl/agent-repl.js +136 -0
  199. package/src/repl/permission-tier.js +28 -0
  200. package/src/runtime/agent-core.js +387 -14
  201. package/src/runtime/coding-agent-contract-shared.cjs +60 -1
  202. package/src/runtime/headless-runner.js +267 -0
  203. package/src/runtime/headless-stream.js +60 -1
  204. package/src/assets/web-panel/assets/ChatBubbleRenderer-C2KLtwHz.js +0 -1
  205. package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +0 -1
  206. package/src/assets/web-panel/assets/Projects-Dn-f5aYe.js +0 -1
  207. package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +0 -1
  208. package/src/assets/web-panel/assets/index-CY1NURWZ.js +0 -1
  209. package/src/assets/web-panel/assets/index-DokUYqpq.js +0 -1
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Persist the explicit turn→checkpoint binding table ([[turn-binding.js]]) as a
3
+ * tamper-evident chained event in the session transcript — the first half of the
4
+ * P1 gap "把绑定作为新链式事件落 jsonl-session-store.js(继承哈希链防篡改,替代
5
+ * 易失的 `_checkpointMarks`)".
6
+ *
7
+ * The REPL's in-process `_checkpointMarks` array is lost the moment the session
8
+ * ends or the process exits, so a resumed session can't offer a coverage-aware
9
+ * rewind for anything from before the restart. Snapshotting the whole
10
+ * `TurnBindingLog` into an append-only `turn_checkpoint_binding` event makes the
11
+ * binding survive resume AND inherit the transcript's hash chain (tamper
12
+ * evidence), exactly like every other session event.
13
+ *
14
+ * All I/O is delegated to the canonical session store via `_deps` so this module
15
+ * stays pure-testable (inject an in-memory append/read pair — no fs, no home dir).
16
+ */
17
+
18
+ import {
19
+ appendEvent as storeAppendEvent,
20
+ readEvents as storeReadEvents,
21
+ } from "../harness/jsonl-session-store.js";
22
+ import {
23
+ TurnBindingLog,
24
+ resolveRestorePlan,
25
+ selectTurnRange,
26
+ RESTORE_SCOPE,
27
+ } from "./turn-binding.js";
28
+
29
+ /** Event type carrying a full TurnBindingLog snapshot. */
30
+ export const TURN_BINDING_EVENT = "turn_checkpoint_binding";
31
+
32
+ // Injected so tests can drive persistence without touching the real sessions dir.
33
+ export const _deps = {
34
+ appendEvent: storeAppendEvent,
35
+ readEvents: storeReadEvents,
36
+ };
37
+
38
+ /**
39
+ * Append the current binding table as a new chained event. Each call writes the
40
+ * FULL table (append-only snapshot), so the newest event is authoritative — a
41
+ * later `loadTurnBindingLog` never has to merge partial deltas across a chain
42
+ * that a compaction could have truncated.
43
+ *
44
+ * Best-effort: a persistence failure must not crash the turn it is recording, so
45
+ * the caller gets `false` instead of a throw.
46
+ *
47
+ * @param {string} sessionId
48
+ * @param {TurnBindingLog|object} log a TurnBindingLog or its toJSON() shape
49
+ * @returns {boolean} true when the snapshot was written
50
+ */
51
+ export function persistTurnBinding(sessionId, log) {
52
+ if (!sessionId || !log) return false;
53
+ const snapshot = log instanceof TurnBindingLog ? log.toJSON() : log;
54
+ if (!snapshot || !Array.isArray(snapshot.turns)) return false;
55
+ try {
56
+ _deps.appendEvent(sessionId, TURN_BINDING_EVENT, snapshot);
57
+ return true;
58
+ } catch {
59
+ // Persistence is advisory — never let it break the agent turn.
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Rebuild the TurnBindingLog from the LATEST persisted snapshot in a session
66
+ * transcript. Returns an empty log when the session has no binding event (or on
67
+ * any read error) so callers can treat "never persisted" and "unreadable" alike.
68
+ *
69
+ * @param {string} sessionId
70
+ * @returns {TurnBindingLog}
71
+ */
72
+ export function loadTurnBindingLog(sessionId) {
73
+ let events = [];
74
+ try {
75
+ events = _deps.readEvents(sessionId) || [];
76
+ } catch {
77
+ return new TurnBindingLog();
78
+ }
79
+ // Newest snapshot wins — scan backwards for the first well-formed one.
80
+ for (let i = events.length - 1; i >= 0; i--) {
81
+ const e = events[i];
82
+ if (
83
+ e &&
84
+ e.type === TURN_BINDING_EVENT &&
85
+ e.data &&
86
+ Array.isArray(e.data.turns)
87
+ ) {
88
+ return TurnBindingLog.fromJSON(e.data);
89
+ }
90
+ }
91
+ return new TurnBindingLog();
92
+ }
93
+
94
+ /**
95
+ * Compose load + plan: resolve the honest restore plan for one turn of a
96
+ * PERSISTED session, so a resumed `/rewind` can show coverage warnings for a
97
+ * turn taken before the restart. Delegates the over-promise guards to
98
+ * `resolveRestorePlan` (unknown turn → a "nothing to restore" plan).
99
+ *
100
+ * @param {string} sessionId
101
+ * @param {string} turnId
102
+ * @param {string} [scope] one of RESTORE_SCOPE
103
+ */
104
+ export function resolveRestorePlanFromSession(
105
+ sessionId,
106
+ turnId,
107
+ scope = RESTORE_SCOPE.BOTH,
108
+ ) {
109
+ const log = loadTurnBindingLog(sessionId);
110
+ return resolveRestorePlan(log.get(turnId), scope);
111
+ }
112
+
113
+ /**
114
+ * Compose load + range select for "Summarize from here / up to here" over a
115
+ * persisted session's turns.
116
+ */
117
+ export function selectTurnRangeFromSession(sessionId, range = {}) {
118
+ return selectTurnRange(loadTurnBindingLog(sessionId), range);
119
+ }
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Turn binding table — explicit `turn → checkpoint` binding + restore planning
3
+ * (P1 "显式绑定 Turn、Checkpoint 和恢复").
4
+ *
5
+ * Today the rewind path binds a turn to its file checkpoint IMPLICITLY: a flat
6
+ * `{atMessageCount, id, tool}` marks array plus the heuristic
7
+ * `pickCheckpointForTurn` in [[repl-rewind.js]]. This module upgrades that to
8
+ * the EXPLICIT table the gap analysis calls for — one record per turn:
9
+ *
10
+ * turnId
11
+ * -> conversationOffset (message index to truncate back to)
12
+ * -> fileCheckpointId (checkpoint-store cp id; see checkpoint-store.js)
13
+ * -> toolCallIds[] (tool_use_ids this turn issued)
14
+ * -> permissionDecisionIds[] (approval decisions made this turn)
15
+ * -> childAgentIds[] (sub-agents spawned this turn)
16
+ * -> worktreeId (isolation worktree, if any)
17
+ * -> coverage: full | partial | none
18
+ *
19
+ * Everything here is PURE (no timers, no I/O, no git): a caller feeds it events
20
+ * from the agent loop (checkpoint, tool-executing, permission decision,
21
+ * spawn_sub_agent, worktree create) and reads back a table + a restore plan.
22
+ *
23
+ * `coverage` is computed HONESTLY — a turn that ran a shell command, spawned an
24
+ * external process, or saw a user edit can never be `full`, because those
25
+ * effects live outside the file tree a checkpoint captures and cannot be
26
+ * promised back. This is the "不能承诺完全恢复" invariant from the gap doc.
27
+ */
28
+
29
+ export const TURN_COVERAGE = Object.freeze({
30
+ FULL: "full",
31
+ PARTIAL: "partial",
32
+ NONE: "none",
33
+ });
34
+
35
+ export const RESTORE_SCOPE = Object.freeze({
36
+ CONVERSATION: "conversation", // conversation only
37
+ FILES: "files", // files only
38
+ BOTH: "both", // conversation and files
39
+ });
40
+
41
+ /**
42
+ * Classify a tool by how restorable its effects are:
43
+ * - "shell" external side-effects (shell / run_code / spawn) — irreversible.
44
+ * - "edit" in-tree file mutation — reversible via a file checkpoint.
45
+ * - "other" read-only / inert.
46
+ * Substring heuristics keep this robust across tool-name spellings.
47
+ */
48
+ export function classifyToolKind(name) {
49
+ const n = String(name || "").toLowerCase();
50
+ if (/shell|bash|(^|_)exec|run_code|spawn|process|subprocess/.test(n)) {
51
+ return "shell";
52
+ }
53
+ if (
54
+ /write|edit|patch|replace|create_file|delete_file|apply|mkdir|move_file/.test(
55
+ n,
56
+ )
57
+ ) {
58
+ return "edit";
59
+ }
60
+ return "other";
61
+ }
62
+
63
+ /**
64
+ * Deterministic coverage from a turn's side-effect flags.
65
+ * side-effects (shell / external process / user edit) → PARTIAL (can't promise)
66
+ * no file mutation → FULL (nothing to undo)
67
+ * mutation WITH a checkpoint → FULL (fully restorable)
68
+ * mutation WITHOUT a checkpoint → NONE (files unrecoverable)
69
+ */
70
+ export function computeCoverage(flags = {}) {
71
+ const sideEffects = Boolean(
72
+ flags.ranShell ||
73
+ flags.spawnedExternalProcess ||
74
+ flags.userEditedDuringTurn,
75
+ );
76
+ if (sideEffects) return TURN_COVERAGE.PARTIAL;
77
+ if (!flags.mutatedFiles) return TURN_COVERAGE.FULL;
78
+ if (flags.hasFileCheckpoint) return TURN_COVERAGE.FULL;
79
+ return TURN_COVERAGE.NONE;
80
+ }
81
+
82
+ function emptyRecord(turnId, conversationOffset) {
83
+ return {
84
+ turnId,
85
+ conversationOffset:
86
+ Number.isFinite(Number(conversationOffset)) && conversationOffset != null
87
+ ? Number(conversationOffset)
88
+ : null,
89
+ fileCheckpointId: null,
90
+ toolCallIds: [],
91
+ permissionDecisionIds: [],
92
+ childAgentIds: [],
93
+ worktreeId: null,
94
+ // side-effect flags → coverage (kept internal; surfaced via `coverage`)
95
+ _flags: {
96
+ ranShell: false,
97
+ spawnedExternalProcess: false,
98
+ userEditedDuringTurn: false,
99
+ mutatedFiles: false,
100
+ hasFileCheckpoint: false,
101
+ },
102
+ };
103
+ }
104
+
105
+ function pushUnique(list, value) {
106
+ const v = value == null ? null : String(value);
107
+ if (v == null || list.includes(v)) return;
108
+ list.push(v);
109
+ }
110
+
111
+ /**
112
+ * The explicit turn-binding table. In-memory + serializable (toJSON/fromJSON)
113
+ * so a caller can persist it alongside the session record — no I/O here.
114
+ */
115
+ export class TurnBindingLog {
116
+ constructor() {
117
+ this._turns = new Map();
118
+ this._order = [];
119
+ }
120
+
121
+ /** Begin (or re-open) a turn record. Idempotent on turnId. */
122
+ startTurn(turnId, { conversationOffset } = {}) {
123
+ const id = String(turnId);
124
+ if (!this._turns.has(id)) {
125
+ this._turns.set(id, emptyRecord(id, conversationOffset));
126
+ this._order.push(id);
127
+ } else if (conversationOffset != null) {
128
+ const rec = this._turns.get(id);
129
+ if (rec.conversationOffset == null) {
130
+ rec.conversationOffset = Number(conversationOffset);
131
+ }
132
+ }
133
+ return this;
134
+ }
135
+
136
+ _rec(turnId) {
137
+ const id = String(turnId);
138
+ if (!this._turns.has(id)) this.startTurn(id);
139
+ return this._turns.get(id);
140
+ }
141
+
142
+ /** Record a tool call; `kind` (or the tool `name`) drives coverage. */
143
+ recordToolCall(turnId, toolCallId, { kind, name } = {}) {
144
+ const rec = this._rec(turnId);
145
+ pushUnique(rec.toolCallIds, toolCallId);
146
+ const k = kind || classifyToolKind(name);
147
+ if (k === "shell") rec._flags.ranShell = true;
148
+ else if (k === "external-process") rec._flags.spawnedExternalProcess = true;
149
+ else if (k === "edit") rec._flags.mutatedFiles = true;
150
+ return this;
151
+ }
152
+
153
+ recordPermissionDecision(turnId, decisionId) {
154
+ pushUnique(this._rec(turnId).permissionDecisionIds, decisionId);
155
+ return this;
156
+ }
157
+
158
+ recordChildAgent(turnId, childAgentId) {
159
+ pushUnique(this._rec(turnId).childAgentIds, childAgentId);
160
+ return this;
161
+ }
162
+
163
+ /** Bind (or re-bind to the earliest) file checkpoint for this turn. */
164
+ bindCheckpoint(turnId, checkpointId) {
165
+ const rec = this._rec(turnId);
166
+ if (checkpointId != null) {
167
+ if (rec.fileCheckpointId == null)
168
+ rec.fileCheckpointId = String(checkpointId);
169
+ rec._flags.hasFileCheckpoint = true;
170
+ }
171
+ return this;
172
+ }
173
+
174
+ setWorktree(turnId, worktreeId) {
175
+ this._rec(turnId).worktreeId =
176
+ worktreeId == null ? null : String(worktreeId);
177
+ return this;
178
+ }
179
+
180
+ /** Flag an out-of-band user edit during this turn → forces PARTIAL coverage. */
181
+ markUserEdit(turnId) {
182
+ this._rec(turnId)._flags.userEditedDuringTurn = true;
183
+ return this;
184
+ }
185
+
186
+ /** One turn record with its computed coverage (or null). */
187
+ get(turnId) {
188
+ const rec = this._turns.get(String(turnId));
189
+ return rec ? this._view(rec) : null;
190
+ }
191
+
192
+ /** All turns in insertion order, each with computed coverage. */
193
+ list() {
194
+ return this._order.map((id) => this._view(this._turns.get(id)));
195
+ }
196
+
197
+ _view(rec) {
198
+ return {
199
+ turnId: rec.turnId,
200
+ conversationOffset: rec.conversationOffset,
201
+ fileCheckpointId: rec.fileCheckpointId,
202
+ toolCallIds: [...rec.toolCallIds],
203
+ permissionDecisionIds: [...rec.permissionDecisionIds],
204
+ childAgentIds: [...rec.childAgentIds],
205
+ worktreeId: rec.worktreeId,
206
+ coverage: computeCoverage(rec._flags),
207
+ };
208
+ }
209
+
210
+ toJSON() {
211
+ return {
212
+ turns: this._order.map((id) => {
213
+ const r = this._turns.get(id);
214
+ return { ...r, _flags: { ...r._flags } };
215
+ }),
216
+ };
217
+ }
218
+
219
+ static fromJSON(json) {
220
+ const log = new TurnBindingLog();
221
+ for (const r of json?.turns || []) {
222
+ if (!r || r.turnId == null) continue;
223
+ const id = String(r.turnId);
224
+ log._turns.set(id, {
225
+ ...emptyRecord(id, r.conversationOffset),
226
+ fileCheckpointId: r.fileCheckpointId ?? null,
227
+ toolCallIds: Array.isArray(r.toolCallIds) ? [...r.toolCallIds] : [],
228
+ permissionDecisionIds: Array.isArray(r.permissionDecisionIds)
229
+ ? [...r.permissionDecisionIds]
230
+ : [],
231
+ childAgentIds: Array.isArray(r.childAgentIds)
232
+ ? [...r.childAgentIds]
233
+ : [],
234
+ worktreeId: r.worktreeId ?? null,
235
+ _flags: { ...emptyRecord(id)._flags, ...(r._flags || {}) },
236
+ });
237
+ log._order.push(id);
238
+ }
239
+ return log;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Plan a restore of one turn under a chosen scope, with HONEST warnings — the
245
+ * restore UI ("Conversation only / Files only / Both") consumes this. It never
246
+ * over-promises: a `partial`/`none` coverage turn, or a scope that would leave
247
+ * files and conversation out of sync, surfaces a warning instead of silently
248
+ * doing a lossy restore.
249
+ *
250
+ * @param {object} turn a record from TurnBindingLog.get()/list()
251
+ * @param {string} scope one of RESTORE_SCOPE
252
+ * @returns {{scope, coverage, conversation:{truncateTo:number}|null,
253
+ * files:{rewindTo:string}|null, warnings:string[]}}
254
+ */
255
+ export function resolveRestorePlan(turn, scope = RESTORE_SCOPE.BOTH) {
256
+ const warnings = [];
257
+ const s = Object.values(RESTORE_SCOPE).includes(scope)
258
+ ? scope
259
+ : RESTORE_SCOPE.BOTH;
260
+ if (!turn) {
261
+ return {
262
+ scope: s,
263
+ coverage: TURN_COVERAGE.NONE,
264
+ conversation: null,
265
+ files: null,
266
+ warnings: ["unknown turn — nothing to restore"],
267
+ };
268
+ }
269
+ const wantConversation =
270
+ s === RESTORE_SCOPE.CONVERSATION || s === RESTORE_SCOPE.BOTH;
271
+ const wantFiles = s === RESTORE_SCOPE.FILES || s === RESTORE_SCOPE.BOTH;
272
+
273
+ let conversation = null;
274
+ if (wantConversation) {
275
+ if (turn.conversationOffset == null) {
276
+ warnings.push(
277
+ "no conversation offset recorded — cannot rewind the conversation",
278
+ );
279
+ } else {
280
+ conversation = { truncateTo: turn.conversationOffset };
281
+ }
282
+ }
283
+
284
+ let files = null;
285
+ if (wantFiles) {
286
+ if (!turn.fileCheckpointId) {
287
+ warnings.push(
288
+ "no file checkpoint for this turn — files cannot be restored",
289
+ );
290
+ } else {
291
+ files = { rewindTo: turn.fileCheckpointId };
292
+ if (turn.coverage === TURN_COVERAGE.PARTIAL) {
293
+ warnings.push(
294
+ "irreversible side-effects (shell / external process / user edits) can't be undone — file restore is best-effort",
295
+ );
296
+ }
297
+ }
298
+ }
299
+
300
+ // Cross-scope drift: restoring one side but not the other leaves them out of
301
+ // sync when this turn touched files.
302
+ const mutatedFiles =
303
+ turn.coverage !== TURN_COVERAGE.FULL || Boolean(turn.fileCheckpointId);
304
+ if (s === RESTORE_SCOPE.CONVERSATION && turn.fileCheckpointId) {
305
+ warnings.push(
306
+ "conversation-only: files will NOT be reverted — the working tree stays ahead of the conversation",
307
+ );
308
+ } else if (
309
+ s === RESTORE_SCOPE.FILES &&
310
+ turn.conversationOffset != null &&
311
+ mutatedFiles
312
+ ) {
313
+ warnings.push(
314
+ "files-only: the conversation is NOT rewound — it will reference a working tree that no longer matches",
315
+ );
316
+ }
317
+
318
+ return { scope: s, coverage: turn.coverage, conversation, files, warnings };
319
+ }
320
+
321
+ /**
322
+ * Select an inclusive turn range for "Summarize from here / up to here". Pure
323
+ * selection over the log's ordered turns; returns the slice plus the
324
+ * conversation offsets that bound it (nulls preserved when a turn lacks one).
325
+ *
326
+ * @returns {{turns:Array, fromOffset:number|null, toOffset:number|null}}
327
+ */
328
+ export function selectTurnRange(
329
+ log,
330
+ { fromTurnId = null, toTurnId = null } = {},
331
+ ) {
332
+ const all =
333
+ log instanceof TurnBindingLog ? log.list() : Array.isArray(log) ? log : [];
334
+ const idxOf = (id) =>
335
+ id == null ? -1 : all.findIndex((t) => t.turnId === String(id));
336
+ let start = fromTurnId == null ? 0 : idxOf(fromTurnId);
337
+ let end = toTurnId == null ? all.length - 1 : idxOf(toTurnId);
338
+ if (start < 0) start = 0;
339
+ if (end < 0) end = all.length - 1;
340
+ if (start > end) [start, end] = [end, start];
341
+ const turns = all.slice(start, end + 1);
342
+ return {
343
+ turns,
344
+ fromOffset: turns.length ? turns[0].conversationOffset : null,
345
+ toOffset: turns.length ? turns[turns.length - 1].conversationOffset : null,
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Bridge: build an explicit TurnBindingLog from the REPL's existing implicit
351
+ * state — `listUserTurns()` output (`{n, index, preview}`, newest-first) and the
352
+ * `{atMessageCount, id, tool}` checkpoint marks. Each mark is attributed to the
353
+ * turn during which it was taken (the turn with the greatest message index still
354
+ * below the mark's `atMessageCount`); the turn's `fileCheckpointId` is its
355
+ * EARLIEST such mark — the pre-mutation snapshot, matching pickCheckpointForTurn.
356
+ *
357
+ * This lets a coverage-aware restore be derived today from what the REPL already
358
+ * tracks, without threading new per-turn events through the agent loop.
359
+ *
360
+ * @param {Array<{n:number,index:number}>} turns
361
+ * @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
362
+ * @returns {TurnBindingLog}
363
+ */
364
+ export function buildTurnBindingFromMarks(turns = [], marks = []) {
365
+ const log = new TurnBindingLog();
366
+ // Ascending by message index so "owning turn" = greatest index below a mark.
367
+ const ordered = [...(turns || [])]
368
+ .filter((t) => t && Number.isFinite(Number(t.index)))
369
+ .sort((a, b) => Number(a.index) - Number(b.index));
370
+ for (const t of ordered) {
371
+ log.startTurn(`turn-${t.index}`, { conversationOffset: Number(t.index) });
372
+ }
373
+ // Group marks by owning turn, then bind earliest-first so bindCheckpoint keeps
374
+ // the pre-mutation snapshot.
375
+ const owned = new Map(); // turnId -> marks[]
376
+ for (const m of marks || []) {
377
+ const c = Number(m?.atMessageCount);
378
+ if (!Number.isFinite(c) || m?.id == null) continue;
379
+ let owner = null;
380
+ for (const t of ordered) {
381
+ if (Number(t.index) < c) owner = t;
382
+ else break;
383
+ }
384
+ if (!owner) continue;
385
+ const key = `turn-${owner.index}`;
386
+ if (!owned.has(key)) owned.set(key, []);
387
+ owned.get(key).push({ ...m, _c: c });
388
+ }
389
+ for (const [turnId, list] of owned) {
390
+ list.sort((a, b) => a._c - b._c);
391
+ for (const m of list) {
392
+ log.recordToolCall(turnId, m.id, { name: m.tool });
393
+ log.bindCheckpoint(turnId, m.id);
394
+ }
395
+ }
396
+ return log;
397
+ }