chainlesschain 0.162.170 → 0.162.171

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 (166) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BTXN24I3.js → AIOps-Cf8yb18C.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BBB1VF4j.js → ActionButton-CsFT6FMt.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-BVCXq0k6.js → Analytics-DQe_hXMA.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-C3EOo2s5.js → AppLayout-Cl9Pp7jk.js} +4 -4
  6. package/src/assets/web-panel/assets/{Artifacts-CsJTZSMv.js → Artifacts-CBEzLVjF.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CP0AllJ_.js → Audit-BS2Un14C.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-BCAjs08m.js → BackgroundAgents-DmcDH8OK.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-BxQI5XH4.js → Backup-DvzjTkkn.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-Bu-PNzC5.js → BaseInput-DnbpuyME.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DjPQynup.js → Chat-BJRRCm2P.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CygkRopu.js → ChatBubbleRenderer-Bgpxyqd_.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BxL6xm7Z.js → Checkbox-DFOrWmvm.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CcyB7h_6.js → Codegen-BIQ2kU6Q.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-8NQiKJJx.js → Col-BI3R-N4I.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-B31qy5OS.js → Community-CPiAwsHT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-C-5fAsZ4.js → Compact-Bv4cslSK.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-BTSccvF0.js → Compliance-CSlDG1NZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BXctU9IK.js → Cowork-CFcRumXf.js} +4 -4
  20. package/src/assets/web-panel/assets/{Cron-CxVFX9NG.js → Cron-DKYKMmOn.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DEF3dq8M.js → Crosschain-CxPMByp2.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BmKcE9he.js → DID-DJqiYw1C.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CECBq_wG.js → Dashboard-D2dLMxqy.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-U-iuPB0n.js → Dropdown-BnJc384A.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D8BIlSPL.js → EmailListRenderer-DctFvm-J.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BzQGfUH-.js → FamilyGuardDashboard-J_xg-Qop.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-4_WlOqLZ.js → Federation-BckL07Uw.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-gtrW-DdF.js → FormItemContext-B8s5ccSO.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BLcvJJB6.js → GenericCardRenderer-hPMBK2No.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-Bk0janGD.js → Git-CfMpYLo7.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CcfyMlV8.js → Governance-Bt3OcrC7.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CBLrAf4M.js → Inference-BlKc336N.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ClguhteZ.js → KnowledgeGraph-B6p7og2c.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-Dp_oeKfJ.js → Logs-uv5Re6YR.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-RQ1HbTY5.js → MarkdownRenderer-CId5JWjX.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-BHk50smM.js → Marketplace-CfNbntHt.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BAY3CV6J.js → McpTools-YRXt74Yk.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-w0pG2AKx.js → Memory-BfQQs-Jd.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-C0VkDUE3.js → MobileBridge-BuGC-4S2.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-BCvQ80I9.js → MobileProjects-Czieqw1m.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-DSIl41Tt.js → Mtc-DHWHOVCO.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-D1mzUlIr.js → MtcAudit-DWSEbMW-.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-CeJz4VSS.js → Multisig-DrhEv1vq.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-_O3aweu8.js → NLProgramming-D8wKCu7E.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Dt7_pO86.js → Notes-AFeqq0I_.js} +4 -4
  46. package/src/assets/web-panel/assets/{NotificationSettings-Dhg3Nktv.js → NotificationSettings-D6qenYUo.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-QsAPcTb2.js → OrderTableRenderer-CF0X4fyB.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-BuiaUPkv.js → Organization-DdfbNqf6.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-C3jb6knl.js → Overflow-BFEvnLfd.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BOMyrLkb.js → P2P-DtL6wbkz.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-UvLStCgS.js → PdhVaultBrowser-CQPriTud.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-B9cN0dJH.js → Permissions-Bj25ANmR.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DNjW5qro.js → PersonalDataHub-BNj2Oux0.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-CFLyZ6Sh.js → Pipeline-Dyj8UEaG.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-D3w6vOxf.js → Privacy-BBUrWRFz.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-cwcVsn8K.js → ProjectInit-BdX7uQn6.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-aUoAroc-.js → ProjectSettings-BghtW1zr.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-CrzQv7Zv.js → Projects-9VTquZCz.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-jZaPizKR.js → Providers-D-0K_Eza.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-DNzmwcfg.js → QrScannerModal-DDaN30_D.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-m-5Gwt70.js → QuickAsk-DfHlifOj.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CrrAjNxP.js → Recommend-CgLIvYE2.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-BDCMjRFe.js → RemoteSession-DpOPrwE4.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-CPCt1Jc8.js → Reputation-wMexlbCE.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-C-RorxHs.js → Row-D9aD_nrc.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-CnQs4TuY.js → RssFeed-Drg7l6zu.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-B1jkeFZY.js → Search-BKO1nrwN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CGKI3vvd.js → Security-G3Kyk-TG.js} +3 -3
  69. package/src/assets/web-panel/assets/{Services-DQjesYF8.js → Services-xJReTOna.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-C9Ipj0A6.js → Skeleton-ldVDVTPG.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-D2j54i9O.js → Skills-emwNYV0j.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-xhrVjTeb.js → Sla-CZYwGp1K.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-CyaPrzOF.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-CmIPU5wR.js → SyncSettings-Dcs5Hhj6.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-B-T1J42r.js → Tasks-CerFAo26.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Aq669sjw.js → Templates-BdZUbOg3.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BKWUWNXB.js → Tenant-B4c_7Bsw.js} +1 -1
  78. package/src/assets/web-panel/assets/Terminal-VVFLS3Ol.js +3 -0
  79. package/src/assets/web-panel/assets/{TimelineRenderer-C465ySdn.js → TimelineRenderer-tc6sp0TV.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-BgVqF-bX.js → Tokens-Bw_vkMJq.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-BMNHmxwH.js → Trigger-DkbJnpvz.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-B_adk89L.js → Trust-CIf9kqN1.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-Biw38mog.js → UkeySign-Cjwllnea.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-AC-eaw5k.js → VideoEditing-kgfkY7AN.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-8lQ5_t0X.js → Wallet-CYWXt4u9.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-JDmH_-DF.js → WebAuthn-QgPwyWCO.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DHQAdITG.js → WorkflowEditor-Co0lha18.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-C3qauUVv.js → chat-CI__Ox-r.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-DLP70JGe.js → colors-Csv0eYYd.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DjsoqA5t.js → compact-item-e53UCdHy.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-CfEdl1in.js → createContext-BMPGRDhV.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-BIulobq7.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DWwHHlBG.js → hasIn-Dz68qIAq.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DbFnfyKh.js → index--7tHEadk.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DKsWujXP.js → index-53ljQo_x.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-ArZCi1rK.js → index-5iicv5af.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DTEcPJGf.js → index-B1s1B8Iy.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BcDeMCUK.js → index-B4lKPJy1.js} +1 -1
  99. package/src/assets/web-panel/assets/index-BGkiYnET.js +1 -0
  100. package/src/assets/web-panel/assets/{index-Ds1BfT_C.js → index-BHkDRNA5.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CNeLedtU.js → index-BOpz2qS1.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Dfka-vzJ.js → index-BT3CV4Nh.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BqPuyN4p.js → index-BW2dImbg.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-C4k6JGm_.js → index-C-NbcAbP.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-D-te-S9f.js → index-C53o-MdJ.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CsEk9s04.js → index-C5hLtxVz.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-OfMwkFIo.js → index-CNtZMPKV.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Bp_WNm2_.js → index-CXuON9OG.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CgruUvB3.js → index-CYUxs6J1.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C8wrUFzY.js → index-CoGSbW5J.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BydBh7oT.js → index-CpTzqJ0Z.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-nuZaMsMy.js → index-CrsoSnQF.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-B-rD2rhi.js → index-CvLBQk_M.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CbF27GqZ.js → index-CyLUaP2i.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DTDpF8MS.js → index-D0bg74e1.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DAcBi6F-.js → index-DCl4Xb4N.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-D5VL0NYy.js → index-DJqQL6so.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-UEbYl2pu.js → index-DVCd2aMX.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-C0xMzvk8.js → index-DctYrVMq.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-C47EiQ7d.js → index-Dk9Gi8xd.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CnhJbWsl.js → index-DnXm3Qsh.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-T5emAyFT.js → index-Dtjm1K1a.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DgQ92xdO.js → index-DyqcHiL1.js} +1 -1
  124. package/src/assets/web-panel/assets/index-Eyhrr_3u.js +1 -0
  125. package/src/assets/web-panel/assets/{index-VtCz3A64.js → index-HbhQRrM2.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B4gCFHtB.js → index-K5E_wjut.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DbjUePgq.js → index-N9DkR1i7.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dn2rNMmv.js → index-_LmPlyeV.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Lh0e2afo.js → index-ajz0E0pK.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Dmt1HDWZ.js → index-t6f80uqL.js} +17 -17
  131. package/src/assets/web-panel/assets/{index-_g5q2hF3.js → index-wcCouSNq.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-gu8zBhtp.js → index-ygfUVVFv.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-C_pMmi75.js → initDefaultProps-DVCeQJrT.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BvKgS96o.js → motion-chBCR_8v.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-C-1U4R1I.js → move-BziPlrh6.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-D6BC968-.js → mtc-parser-Cenk0OD7.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-DxOZvcGQ.js → omit-D-AiRsei.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-LpzmHdkw.js → pickAttrs-CHw3Fvqt.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-Bm--qom1.js → placementArrow-g8I-rCad.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-CVZZlhJf.js → responsiveObserve-BWffAr9k.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-DD1xINGx.js → slide-BmUQqhkb.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-LrjHAy63.js → statusUtils-B89gCO-y.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-BqknYxTe.js → styleChecker-x1bnCF73.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-Dljy3diT.js → useFlexGapSupport-B-k1_Ggw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-OedUtPLc.js → useFs-Bc3PUqQk.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-Cz-kxbP_.js → usePersonalDataHub-D8qFDX1I.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-C8ZuTk8W.js → vnode-UaL5-WGg.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-CjbgJYXQ.js → zoom-Dv7bsbLs.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agent.js +39 -14
  151. package/src/commands/background-session.js +12 -1
  152. package/src/gateways/ws/ws-agent-handler.js +26 -1
  153. package/src/gateways/ws/ws-approval-gate.js +131 -0
  154. package/src/lib/agent-worktree.js +173 -5
  155. package/src/lib/background-agent-supervisor.js +193 -4
  156. package/src/lib/interaction-adapter.js +26 -5
  157. package/src/lib/repl-rewind.js +62 -14
  158. package/src/lib/turn-binding.js +134 -0
  159. package/src/repl/agent-repl.js +75 -3
  160. package/src/repl/repl-turn-binding.js +91 -0
  161. package/src/runtime/headless-runner.js +59 -0
  162. package/src/assets/web-panel/assets/SpeechSettings-C6KMOUCa.js +0 -1
  163. package/src/assets/web-panel/assets/Terminal-DEeAvA8L.js +0 -3
  164. package/src/assets/web-panel/assets/devWarning-Cwuuqc_f.js +0 -1
  165. package/src/assets/web-panel/assets/index-BkTk-zJK.js +0 -1
  166. package/src/assets/web-panel/assets/index-C-BSTEhe.js +0 -1
@@ -187,7 +187,7 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
187
187
  });
188
188
  }
189
189
 
190
- _ask(questionType, question, extra = {}) {
190
+ _ask(questionType, question, extra = {}, options = {}) {
191
191
  return this._request(
192
192
  {
193
193
  type: "question",
@@ -195,7 +195,12 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
195
195
  question,
196
196
  ...extra,
197
197
  },
198
- { kind: "question" },
198
+ {
199
+ kind: "question",
200
+ // Approval binding pass-through: a permission-gate producer supplies
201
+ // it; plain questions never carry one (message stays byte-identical).
202
+ ...(options.binding ? { binding: options.binding } : {}),
203
+ },
199
204
  );
200
205
  }
201
206
 
@@ -207,10 +212,26 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
207
212
  return this._ask("select", question, { choices });
208
213
  }
209
214
 
215
+ /**
216
+ * @param {string} question
217
+ * @param {boolean|object} [defaultVal] — boolean (legacy, byte-identical) or
218
+ * an options object `{ default, binding, ...extra }`: `binding` rides out
219
+ * on the request so the client must echo it back on approve (verified in
220
+ * _resolvePending, mismatched approve → deny); any other keys merge into
221
+ * the question message (e.g. structured `approval` context for UIs).
222
+ */
210
223
  async askConfirm(question, defaultVal = true) {
211
- const answer = await this._ask("confirm", question, {
212
- default: defaultVal,
213
- });
224
+ const opts =
225
+ defaultVal !== null && typeof defaultVal === "object"
226
+ ? defaultVal
227
+ : { default: defaultVal };
228
+ const { binding = null, default: dflt = true, ...extra } = opts;
229
+ const answer = await this._ask(
230
+ "confirm",
231
+ question,
232
+ { default: dflt, ...extra },
233
+ binding ? { binding } : {},
234
+ );
214
235
  // Normalize to boolean
215
236
  if (typeof answer === "boolean") return answer;
216
237
  return answer === "true" || answer === "yes" || answer === "y";
@@ -232,18 +232,48 @@ export function restoreClearedConversation(messages, marks, cleared) {
232
232
  }
233
233
 
234
234
  /**
235
- * Coverage-aware restore plan for a rewound turn, derived from the REPL's
236
- * EXISTING implicit state (`listUserTurns()` output + `{atMessageCount,id,tool}`
237
- * marks) — no new agent-loop events required. Bridges those into the explicit
238
- * turn-binding table and asks `resolveRestorePlan` for the honest plan, so
239
- * `/rewind` can warn when a restore can't be fully promised (side-effects, a
240
- * missing checkpoint, or a conversation/files drift) instead of silently doing a
241
- * lossy rewind.
235
+ * Match a live-conversation turn to its record in a PERSISTED explicit
236
+ * turn-binding table (`loadTurnBindingLog(sessionId)` — produced by headless
237
+ * runs of the same session, which anchor each turn at
238
+ * `conversationOffset = messages.length` measured just AFTER that turn's user
239
+ * message was appended). So the record for the user message at live index
240
+ * `turnIndex` is the one with `conversationOffset === turnIndex + 1`. Exact
241
+ * match only — anything else falls back to the marks-derived table rather than
242
+ * guessing (the offsets align because both runs rebuild the same persisted
243
+ * message sequence under a single leading system prompt).
244
+ *
245
+ * @param {import("./turn-binding.js").TurnBindingLog|null} persistedLog
246
+ * @param {number} turnIndex message index of the target user turn
247
+ * @returns {object|null} a turn view (with computed coverage), or null
248
+ */
249
+ export function pickPersistedTurn(persistedLog, turnIndex) {
250
+ if (!persistedLog || typeof persistedLog.list !== "function") return null;
251
+ const want = Number(turnIndex) + 1;
252
+ if (!Number.isFinite(want)) return null;
253
+ for (const t of persistedLog.list()) {
254
+ if (Number(t.conversationOffset) === want) return t;
255
+ }
256
+ return null;
257
+ }
258
+
259
+ /**
260
+ * Coverage-aware restore plan for a rewound turn. Prefers the PERSISTED
261
+ * explicit turn-binding table when the session has one for the target turn
262
+ * (cross-process honesty: a `--resume`d session's pre-resume turns have NO
263
+ * process-local marks, but their headless run persisted checkpoint ids, shell/
264
+ * side-effect flags and child agents — the explicit table answers where marks
265
+ * cannot). Falls back to the REPL's implicit state (`listUserTurns()` output +
266
+ * `{atMessageCount,id,tool}` marks) bridged into the same table shape. Either
267
+ * way `resolveRestorePlan` produces the honest plan, so `/rewind` can warn
268
+ * when a restore can't be fully promised (side-effects, a missing checkpoint,
269
+ * or a conversation/files drift) instead of silently doing a lossy rewind.
242
270
  *
243
271
  * @param {Array} messages live conversation (for listUserTurns)
244
272
  * @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
245
273
  * @param {number} turnIndex message index returned by rewindToTurn()
246
274
  * @param {string} [scope] one of RESTORE_SCOPE (default BOTH)
275
+ * @param {{persistedLog?:object|null}} [opts] explicit table from
276
+ * `loadTurnBindingLog(sessionId)` (advisory; null → marks only)
247
277
  * @returns {{scope, coverage, conversation, files, warnings:string[]}}
248
278
  */
249
279
  export function buildRewindPlan(
@@ -251,7 +281,10 @@ export function buildRewindPlan(
251
281
  marks,
252
282
  turnIndex,
253
283
  scope = RESTORE_SCOPE.BOTH,
284
+ { persistedLog = null } = {},
254
285
  ) {
286
+ const persisted = pickPersistedTurn(persistedLog, turnIndex);
287
+ if (persisted) return resolveRestorePlan(persisted, scope);
255
288
  const turns = listUserTurns(messages, { limit: 1000 });
256
289
  const log = buildTurnBindingFromMarks(turns, marks);
257
290
  const turn = log.get(`turn-${Number(turnIndex)}`);
@@ -288,18 +321,29 @@ export function renderRewindWarnings(plan) {
288
321
  * @param {Array} messages live conversation (for listUserTurns)
289
322
  * @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
290
323
  * @param {number} turnIndex message index of the branch-point user turn
291
- * @param {{parentSessionId?:string, seq?:number, writeIntent?:boolean}} [opts]
324
+ * @param {{parentSessionId?:string, seq?:number, writeIntent?:boolean,
325
+ * persistedLog?:object|null}} [opts] persistedLog: explicit table
326
+ * from `loadTurnBindingLog(sessionId)`, preferred over marks (same
327
+ * cross-process rationale as buildRewindPlan)
292
328
  * @returns {object} the plan from planSessionBranch()
293
329
  */
294
330
  export function buildBranchPlan(
295
331
  messages,
296
332
  marks,
297
333
  turnIndex,
298
- { parentSessionId = null, seq = 0, writeIntent = false } = {},
334
+ {
335
+ parentSessionId = null,
336
+ seq = 0,
337
+ writeIntent = false,
338
+ persistedLog = null,
339
+ } = {},
299
340
  ) {
300
- const turns = listUserTurns(messages, { limit: 1000 });
301
- const log = buildTurnBindingFromMarks(turns, marks);
302
- const turn = log.get(`turn-${Number(turnIndex)}`);
341
+ let turn = pickPersistedTurn(persistedLog, turnIndex);
342
+ if (!turn) {
343
+ const turns = listUserTurns(messages, { limit: 1000 });
344
+ const log = buildTurnBindingFromMarks(turns, marks);
345
+ turn = log.get(`turn-${Number(turnIndex)}`);
346
+ }
303
347
  return planSessionBranch({ parentSessionId, turn, seq, writeIntent });
304
348
  }
305
349
 
@@ -336,6 +380,7 @@ export function renderBranchPlan(plan) {
336
380
  * /rewind --files 3 → { command: "turn", n: 3, scope: FILES }
337
381
  * /rewind 3 --branch → { command: "branch", n: 3, writeIntent: false }
338
382
  * /rewind 3 --branch --write → { command: "branch", n: 3, writeIntent: true }
383
+ * /rewind 3 --both --yes → non-interactive file confirmation (IDE)
339
384
  *
340
385
  * An unknown flag is ignored (kept forgiving); a non-numeric, flag-less arg with
341
386
  * no turn number falls back to `list` so a typo can't silently rewind — and a
@@ -354,6 +399,7 @@ export function parseRewindArg(arg) {
354
399
  let scope = RESTORE_SCOPE.BOTH;
355
400
  let branch = false;
356
401
  let writeIntent = false;
402
+ let confirmed = false;
357
403
  for (const t of tokens) {
358
404
  if (t === "--files" || t === "--files-only") scope = RESTORE_SCOPE.FILES;
359
405
  else if (
@@ -365,11 +411,13 @@ export function parseRewindArg(arg) {
365
411
  else if (t === "--both") scope = RESTORE_SCOPE.BOTH;
366
412
  else if (t === "--branch" || t === "--fork") branch = true;
367
413
  else if (t === "--write" || t === "--write-intent") writeIntent = true;
414
+ else if (t === "--yes" || t === "-y") confirmed = true;
368
415
  else if (n === null && /^\d+$/.test(t)) n = Number(t);
369
416
  }
370
- if (branch && n !== null) return { command: "branch", n, scope, writeIntent };
417
+ if (branch && n !== null)
418
+ return { command: "branch", n, scope, writeIntent, confirmed };
371
419
  if (n === null) return { command: "list", scope };
372
- return { command: "turn", n, scope };
420
+ return { command: "turn", n, scope, confirmed };
373
421
  }
374
422
 
375
423
  /** Render the picker list (shared by /rewind and double-Esc). */
@@ -183,6 +183,35 @@ export class TurnBindingLog {
183
183
  return this;
184
184
  }
185
185
 
186
+ /**
187
+ * Drop every turn anchored AT or AFTER `offset` — the timeline-supersede rule
188
+ * for a live producer. After a `/rewind`, `/clear`, or compaction shrinks the
189
+ * live conversation, a new turn re-anchors at an offset an OLD record may
190
+ * still hold; under exact-offset matching (see `pickPersistedTurn`) that stale
191
+ * record would shadow the new one and could offer the wrong checkpoint. The
192
+ * discarded-timeline records are pruned instead — correctness over advisory
193
+ * completeness.
194
+ *
195
+ * @param {number} offset conversation offset the new timeline starts at
196
+ * @returns {number} how many records were removed
197
+ */
198
+ pruneFromOffset(offset) {
199
+ const at = Number(offset);
200
+ if (!Number.isFinite(at)) return 0;
201
+ let removed = 0;
202
+ this._order = this._order.filter((id) => {
203
+ const rec = this._turns.get(id);
204
+ const off = rec ? Number(rec.conversationOffset) : NaN;
205
+ if (Number.isFinite(off) && off >= at) {
206
+ this._turns.delete(id);
207
+ removed += 1;
208
+ return false;
209
+ }
210
+ return true;
211
+ });
212
+ return removed;
213
+ }
214
+
186
215
  /** One turn record with its computed coverage (or null). */
187
216
  get(turnId) {
188
217
  const rec = this._turns.get(String(turnId));
@@ -395,3 +424,108 @@ export function buildTurnBindingFromMarks(turns = [], marks = []) {
395
424
  }
396
425
  return log;
397
426
  }
427
+
428
+ /**
429
+ * Stateful feeder turning raw agent-loop events into TurnBindingLog records —
430
+ * the SHARED producer core for every runner (headless and the interactive
431
+ * REPL), so id synthesis and the event→table mapping can never drift between
432
+ * them. Provider tool_use ids are not visible on the yielded event stream, so
433
+ * tool-call ids are synthesized `<nonce>:c<seq>` and turn ids `<nonce>:t<seq>`
434
+ * (mirrors the side-effect ledger's op-id scheme — stable within a run, unique
435
+ * across resumes as long as `nonce` is per-run unique).
436
+ *
437
+ * Dirty-gating: `beginTurn` alone never marks the table dirty, so a tool-free
438
+ * turn costs the caller zero persistence writes; only a recorded signal
439
+ * (checkpoint / tool call / permission decision / child agent / a supersede
440
+ * prune) does.
441
+ *
442
+ * @param {{log?: TurnBindingLog, nonce?: string}} [opts]
443
+ */
444
+ export function createTurnBindingFeed({
445
+ log = new TurnBindingLog(),
446
+ nonce = "run",
447
+ } = {}) {
448
+ let turnSeq = 0;
449
+ let callSeq = 0;
450
+ let currentTurnId = null;
451
+ let currentCallId = null;
452
+ let dirty = false;
453
+ return {
454
+ log,
455
+ /**
456
+ * Anchor a new turn at `conversationOffset` (= messages.length measured
457
+ * just AFTER the turn's user message was appended — the exact-match
458
+ * contract `pickPersistedTurn` relies on). `supersede: true` first prunes
459
+ * records at/after that offset (live-REPL rewind/clear/compaction rule);
460
+ * append-only runners leave it off. `worktreeId` stamps the run's isolation
461
+ * worktree onto the record (does NOT mark dirty by itself — a tool-free
462
+ * turn in a worktree still writes nothing).
463
+ */
464
+ beginTurn(
465
+ conversationOffset,
466
+ { supersede = false, worktreeId = null } = {},
467
+ ) {
468
+ if (supersede && log.pruneFromOffset(conversationOffset) > 0) {
469
+ dirty = true;
470
+ }
471
+ currentTurnId = `${nonce}:t${turnSeq++}`;
472
+ log.startTurn(currentTurnId, { conversationOffset });
473
+ if (worktreeId != null) log.setWorktree(currentTurnId, worktreeId);
474
+ return currentTurnId;
475
+ },
476
+ currentTurnId: () => currentTurnId,
477
+ /** Fold one agent-loop event into the current turn's record. */
478
+ handleEvent(event) {
479
+ if (!event || !currentTurnId) return dirty;
480
+ const turnId = currentTurnId;
481
+ switch (event.type) {
482
+ case "checkpoint":
483
+ if (event.id != null) {
484
+ log.bindCheckpoint(turnId, event.id);
485
+ dirty = true;
486
+ }
487
+ break;
488
+ case "tool-executing": {
489
+ const callId = `${nonce}:c${callSeq++}`;
490
+ currentCallId = callId;
491
+ log.recordToolCall(turnId, callId, { name: event.tool });
492
+ dirty = true;
493
+ break;
494
+ }
495
+ case "tool-result": {
496
+ // A gated tool surfaces its permission decision on `result.policy`;
497
+ // re-derive a stable decision id feeder-side (core's own id hangs
498
+ // off the provider call id, which the event stream does not expose).
499
+ const policy = event.result?.policy;
500
+ if (policy && (policy.via || policy.decision)) {
501
+ log.recordPermissionDecision(
502
+ turnId,
503
+ `${currentCallId || "call"}:perm:${policy.via || policy.decision}`,
504
+ );
505
+ dirty = true;
506
+ }
507
+ // Foreground spawn_sub_agent reports its child id on the result.
508
+ if (event.result?.subAgentId) {
509
+ log.recordChildAgent(turnId, event.result.subAgentId);
510
+ dirty = true;
511
+ }
512
+ currentCallId = null;
513
+ break;
514
+ }
515
+ case "background-sub-agent-result":
516
+ if (event.subAgentId) {
517
+ log.recordChildAgent(turnId, event.subAgentId);
518
+ dirty = true;
519
+ }
520
+ break;
521
+ default:
522
+ break;
523
+ }
524
+ return dirty;
525
+ },
526
+ isDirty: () => dirty,
527
+ clearDirty() {
528
+ dirty = false;
529
+ },
530
+ };
531
+ }
@@ -295,6 +295,17 @@ export async function agentLoop(messages, options) {
295
295
  autoCompact: false,
296
296
  onProviderFallback,
297
297
  })) {
298
+ // P1 explicit turn→checkpoint binding — the REPL as PRODUCER: fold every
299
+ // loop event into the live table (checkpoint / tool / policy / child
300
+ // agent), mirroring the headless runner's feed. Advisory: a feeder failure
301
+ // must never disturb the turn. Absent by default → byte-identical.
302
+ if (options.turnBindingFeed) {
303
+ try {
304
+ options.turnBindingFeed.handleEvent(event);
305
+ } catch {
306
+ /* advisory — never break the turn */
307
+ }
308
+ }
298
309
  if (event.type === "checkpoint") {
299
310
  // Remember which file snapshot lines up with the live conversation so
300
311
  // `/rewind <n>` can restore code + conversation together (Claude-Code
@@ -1078,6 +1089,11 @@ export async function startAgentRepl(options = {}) {
1078
1089
  // it back (Claude-Code 2.1.191). Nulled on a context swap so a stash from one
1079
1090
  // session can't be restored into another. { messages, marks } | null.
1080
1091
  let _clearedConversation = null;
1092
+ // P1 explicit turn→checkpoint binding — the REPL as PRODUCER (mirrors the
1093
+ // headless runner's feed via the shared feeder core): one record per
1094
+ // interactive turn, persisted dirty-gated at each settle. Lazily created on
1095
+ // the first prompt turn of a persistable (JSONL) session; advisory only.
1096
+ let _turnBindingProducer = null;
1081
1097
  // Apply --output-style or the settings.json `outputStyle` default at startup.
1082
1098
  try {
1083
1099
  const { resolveOutputStyle } = await import("../lib/output-styles.js");
@@ -3120,6 +3136,7 @@ export async function startAgentRepl(options = {}) {
3120
3136
  buildBranchPlan,
3121
3137
  renderBranchPlan,
3122
3138
  parseRewindArg,
3139
+ pickPersistedTurn,
3123
3140
  RESTORE_SCOPE,
3124
3141
  } = await import("../lib/repl-rewind.js");
3125
3142
  const arg = trimmed.slice("/rewind".length).trim();
@@ -3127,6 +3144,24 @@ export async function startAgentRepl(options = {}) {
3127
3144
  // --files / --both). Claude-Code parity: a rewind can restore the
3128
3145
  // conversation only, the files only, or both.
3129
3146
  const parsed = parseRewindArg(arg);
3147
+ // P1 turn/checkpoint binding: load the session's PERSISTED explicit
3148
+ // turn-binding table (headless runs of this session feed it). A
3149
+ // --resume'd session's pre-resume turns have no process-local marks —
3150
+ // the explicit table is the only source of their checkpoint ids and
3151
+ // side-effect coverage. Best-effort advisory: never blocks /rewind.
3152
+ let _persistedBinding = null;
3153
+ if (
3154
+ sessionId &&
3155
+ (parsed.command === "turn" || parsed.command === "branch")
3156
+ ) {
3157
+ try {
3158
+ const { loadTurnBindingLog } =
3159
+ await import("../lib/turn-binding-store.js");
3160
+ _persistedBinding = loadTurnBindingLog(sessionId);
3161
+ } catch {
3162
+ /* marks-only fallback */
3163
+ }
3164
+ }
3130
3165
  if (parsed.command === "clear") {
3131
3166
  // Restore the conversation stashed by /clear (Claude-Code 2.1.191).
3132
3167
  const r = restoreClearedConversation(
@@ -3173,6 +3208,7 @@ export async function startAgentRepl(options = {}) {
3173
3208
  {
3174
3209
  parentSessionId: sessionId,
3175
3210
  writeIntent: parsed.writeIntent,
3211
+ persistedLog: _persistedBinding,
3176
3212
  },
3177
3213
  );
3178
3214
  if (!plan || !plan.branchSessionId) {
@@ -3277,6 +3313,7 @@ export async function startAgentRepl(options = {}) {
3277
3313
  _checkpointMarks,
3278
3314
  turnIndex,
3279
3315
  scope,
3316
+ { persistedLog: _persistedBinding },
3280
3317
  );
3281
3318
  for (const line of renderRewindWarnings(plan))
3282
3319
  logger.log(chalk.yellow(line));
@@ -3286,11 +3323,21 @@ export async function startAgentRepl(options = {}) {
3286
3323
  // 3) File restore (skipped for conversation-only). Match the turn to
3287
3324
  // the snapshot taken just before it first mutated the tree, then
3288
3325
  // offer to roll the working tree back to it (undoable — the restore
3289
- // takes its own safety checkpoint first). Prune dropped-turn marks
3290
- // only when the conversation was actually truncated.
3291
- const cp = restoreFiles
3326
+ // takes its own safety checkpoint first). Process-local marks win;
3327
+ // a --resume'd turn with no marks falls back to the checkpoint id
3328
+ // the PERSISTED binding table recorded for it (same checkpoint
3329
+ // store, so cross-process file rewind actually works — restore
3330
+ // still fails soft if the snapshot is gone). Prune dropped-turn
3331
+ // marks only when the conversation was actually truncated.
3332
+ let cp = restoreFiles
3292
3333
  ? pickCheckpointForTurn(_checkpointMarks, turnIndex)
3293
3334
  : null;
3335
+ if (!cp && restoreFiles) {
3336
+ const persisted = pickPersistedTurn(_persistedBinding, turnIndex);
3337
+ if (persisted?.fileCheckpointId) {
3338
+ cp = { id: persisted.fileCheckpointId, fromPersisted: true };
3339
+ }
3340
+ }
3294
3341
  if (rewindConversation)
3295
3342
  pruneMarksAfter(_checkpointMarks, turnIndex);
3296
3343
  if (cp) {
@@ -5159,6 +5206,25 @@ export async function startAgentRepl(options = {}) {
5159
5206
  const _userMsg = { role: "user", content: _userMessageContent };
5160
5207
  messages.push(_userMsg);
5161
5208
 
5209
+ // P1 turn→checkpoint binding: anchor this turn in the explicit table at
5210
+ // `conversationOffset = messages.length` measured just AFTER the user
5211
+ // message was appended (the exact-match contract pickPersistedTurn relies
5212
+ // on — same anchor as the headless runner). Only JSONL sessions can
5213
+ // persist the table; the producer supersedes stale records from a
5214
+ // discarded timeline (post-/rewind, /clear, compaction) itself.
5215
+ if (sessionId && useJsonl) {
5216
+ try {
5217
+ if (!_turnBindingProducer) {
5218
+ const { createReplTurnBindingProducer } =
5219
+ await import("./repl-turn-binding.js");
5220
+ _turnBindingProducer = createReplTurnBindingProducer({ sessionId });
5221
+ }
5222
+ _turnBindingProducer?.beginTurn(messages.length);
5223
+ } catch {
5224
+ /* advisory — never disturb the turn */
5225
+ }
5226
+ }
5227
+
5162
5228
  // Slot-filling: detect intent and fill missing parameters interactively
5163
5229
  try {
5164
5230
  const { CLISlotFiller } = await import("../lib/slot-filler.js");
@@ -5368,6 +5434,9 @@ export async function startAgentRepl(options = {}) {
5368
5434
  autoCheckpoint,
5369
5435
  checkpointSession: sessionId,
5370
5436
  checkpointMarks: _checkpointMarks,
5437
+ // Explicit turn-binding producer (null unless a JSONL session): the
5438
+ // wrapper folds every loop event into the live table.
5439
+ turnBindingFeed: _turnBindingProducer,
5371
5440
  denialLog: _recentDenials,
5372
5441
  persistRecentDenials: true,
5373
5442
  permissionMode: _sessionTier,
@@ -5589,6 +5658,9 @@ export async function startAgentRepl(options = {}) {
5589
5658
  // Non-critical
5590
5659
  }
5591
5660
  }
5661
+ // Persist the explicit turn-binding table for this settled turn
5662
+ // (dirty-gated: a tool-free Q&A turn writes nothing; never throws).
5663
+ _turnBindingProducer?.persistIfDirty();
5592
5664
  // Auto-compact when context grows too large
5593
5665
  if (
5594
5666
  feature("PROMPT_COMPRESSOR") &&
@@ -0,0 +1,91 @@
1
+ /**
2
+ * REPL turn-binding PRODUCER — closes the incremental-gap 仍欠 item "REPL 自身
3
+ * 作为显式表生产者(交互 turn 实时喂+持久化,镜像 headless 接线;现 REPL 只消费)".
4
+ *
5
+ * Wraps the SHARED feeder core ([[turn-binding.js]] `createTurnBindingFeed` —
6
+ * the exact mapping the headless runner uses, so the two producers can never
7
+ * drift) with the three REPL-specific concerns:
8
+ *
9
+ * 1. Rehydrate: the first turn loads the session's persisted table
10
+ * (`loadTurnBindingLog`) so an interactive continuation of a headless (or
11
+ * earlier REPL) run APPENDS to the existing table instead of clobbering it.
12
+ * 2. Timeline supersede: the live REPL can shrink its conversation
13
+ * (`/rewind`, `/clear`, auto-compaction), so each new turn prunes records
14
+ * anchored at/after its own offset — under `pickPersistedTurn`'s
15
+ * exact-offset matching a stale record from the discarded timeline would
16
+ * otherwise shadow the new turn and could offer the wrong checkpoint.
17
+ * 3. Per-settle persistence: dirty-gated `persistTurnBinding` after each turn
18
+ * (a tool-free Q&A turn writes nothing), best-effort — the producer is
19
+ * advisory and must NEVER disturb the turn itself, so every method
20
+ * swallows its own failures.
21
+ *
22
+ * I/O goes through `_deps` so tests drive the producer hermetically.
23
+ */
24
+
25
+ import { createTurnBindingFeed } from "../lib/turn-binding.js";
26
+ import {
27
+ loadTurnBindingLog,
28
+ persistTurnBinding,
29
+ } from "../lib/turn-binding-store.js";
30
+
31
+ // Injected so tests can seed the persisted table / capture snapshots without
32
+ // touching the real sessions directory.
33
+ export const _deps = {
34
+ loadTurnBindingLog,
35
+ persistTurnBinding,
36
+ now: () => Date.now(),
37
+ };
38
+
39
+ /**
40
+ * @param {{sessionId?: string, nonce?: string}} [opts]
41
+ * @returns {object|null} null when the session cannot persist (no sessionId)
42
+ */
43
+ export function createReplTurnBindingProducer({ sessionId, nonce } = {}) {
44
+ if (!sessionId) return null;
45
+ // Per-process nonce (mirrors the headless runner's per-run nonce): restored
46
+ // turn ids keep their original nonce, new ids can't collide across resumes.
47
+ const runNonce = nonce || String(_deps.now());
48
+ let feed = null;
49
+ return {
50
+ /**
51
+ * Anchor a new interactive turn. Call just AFTER the user message was
52
+ * appended, with `conversationOffset = messages.length` — the exact-match
53
+ * contract `pickPersistedTurn` relies on.
54
+ */
55
+ beginTurn(conversationOffset) {
56
+ try {
57
+ if (!feed) {
58
+ feed = createTurnBindingFeed({
59
+ log: _deps.loadTurnBindingLog(sessionId),
60
+ nonce: runNonce,
61
+ });
62
+ }
63
+ return feed.beginTurn(conversationOffset, { supersede: true });
64
+ } catch {
65
+ return null; // advisory — never disturb the turn
66
+ }
67
+ },
68
+ /** Fold one agent-loop event into the current turn (fed by agentLoop). */
69
+ handleEvent(event) {
70
+ if (!feed) return false;
71
+ try {
72
+ return feed.handleEvent(event);
73
+ } catch {
74
+ return false;
75
+ }
76
+ },
77
+ /** Persist the table after a settled turn (dirty-gated, best-effort). */
78
+ persistIfDirty() {
79
+ if (!feed || !feed.isDirty()) return false;
80
+ try {
81
+ const ok = _deps.persistTurnBinding(sessionId, feed.log);
82
+ if (ok) feed.clearDirty();
83
+ return ok;
84
+ } catch {
85
+ return false;
86
+ }
87
+ },
88
+ /** The live table (for tests / future in-process consumers). */
89
+ log: () => (feed ? feed.log : null),
90
+ };
91
+ }
@@ -57,6 +57,8 @@ import {
57
57
  classifyToolSideEffect,
58
58
  } from "../lib/side-effect-ledger.js";
59
59
  import { SIDE_EFFECT_LEDGER_EVENT } from "../lib/side-effect-ledger-store.js";
60
+ import { TurnBindingLog, createTurnBindingFeed } from "../lib/turn-binding.js";
61
+ import { TURN_BINDING_EVENT } from "../lib/turn-binding-store.js";
60
62
  import { operationIdempotencyKey } from "../lib/idempotency.js";
61
63
  import { expandFileRefsAsync } from "./file-ref-expander.js";
62
64
  import { composeSystemPrompt } from "./system-prompt.js";
@@ -730,6 +732,50 @@ export async function runAgentHeadless(options = {}, deps = {}) {
730
732
  // best-effort — never fail the run over ledger persistence
731
733
  }
732
734
  };
735
+ // P1 explicit turn→checkpoint binding: feed the TurnBindingLog in real time
736
+ // off the yielded agent-loop events (checkpoint / tool / policy / child-agent)
737
+ // and persist the table as a hash-chained `turn_checkpoint_binding` session
738
+ // event at each turn boundary. Provider tool_use ids are not runner-visible,
739
+ // so tool-call ids are synthesized `<nonce>:c<seq>` (mirrors the side-effect
740
+ // ledger's op-id scheme — stable within a run, unique across resumes).
741
+ // rebuildMessages ignores this event type, so it never pollutes model
742
+ // context. Only writes when a turn actually recorded something, and only for
743
+ // persisted runs — a tool-free Q&A or ephemeral run stays byte-identical.
744
+ let turnBindingLog = new TurnBindingLog();
745
+ if (persist) {
746
+ try {
747
+ const events = store.readEvents(sessionId) || [];
748
+ for (let i = events.length - 1; i >= 0; i--) {
749
+ const e = events[i];
750
+ if (
751
+ e &&
752
+ e.type === TURN_BINDING_EVENT &&
753
+ e.data &&
754
+ Array.isArray(e.data.turns)
755
+ ) {
756
+ turnBindingLog = TurnBindingLog.fromJSON(e.data);
757
+ break;
758
+ }
759
+ }
760
+ } catch {
761
+ // fresh log — best effort
762
+ }
763
+ }
764
+ // Event→table folding + id synthesis live in the SHARED feeder core
765
+ // (createTurnBindingFeed) so the interactive REPL producer can never drift
766
+ // from this runner's mapping. No feed at all for non-persisted runs.
767
+ const turnBindingFeed = persist
768
+ ? createTurnBindingFeed({ log: turnBindingLog, nonce: sideEffectRunNonce })
769
+ : null;
770
+ const persistTurnBindingLog = () => {
771
+ if (!turnBindingFeed || !turnBindingFeed.isDirty()) return;
772
+ try {
773
+ store.appendEvent(sessionId, TURN_BINDING_EVENT, turnBindingLog.toJSON());
774
+ turnBindingFeed.clearDirty();
775
+ } catch {
776
+ // best-effort — never fail the run over binding persistence
777
+ }
778
+ };
733
779
  if (persist) {
734
780
  try {
735
781
  const events = store.readEvents(sessionId) || [];
@@ -1703,7 +1749,17 @@ export async function runAgentHeadless(options = {}, deps = {}) {
1703
1749
  try {
1704
1750
  // eslint-disable-next-line no-constant-condition
1705
1751
  while (true) {
1752
+ // P1 turn→checkpoint binding: one drain of the loop = one turn. Anchor
1753
+ // the conversation offset before the model sees the new user turn. A
1754
+ // `--worktree` run stamps its isolation worktree (branch name, threaded
1755
+ // from the command layer) onto each turn record.
1756
+ if (turnBindingFeed) {
1757
+ turnBindingFeed.beginTurn(messages.length, {
1758
+ worktreeId: options.worktreeId ?? null,
1759
+ });
1760
+ }
1706
1761
  for await (const event of runLoop(messages, loopOptions)) {
1762
+ if (turnBindingFeed) turnBindingFeed.handleEvent(event);
1707
1763
  switch (event.type) {
1708
1764
  case "checkpoint": {
1709
1765
  if (isText)
@@ -1876,6 +1932,9 @@ export async function runAgentHeadless(options = {}, deps = {}) {
1876
1932
  // call is made (break out of the for-await, not just the switch).
1877
1933
  if (stopForCost) break;
1878
1934
  }
1935
+ // P1 turn→checkpoint binding: the turn settled — persist the explicit
1936
+ // table (latest snapshot wins on load; no-op when nothing was recorded).
1937
+ persistTurnBindingLog();
1879
1938
  // ── auto-rewake re-drive (opt-in via --auto-rewake) ─────────────────
1880
1939
  // Under auto-rewake, settle the async Stop hooks after EVERY turn (so the
1881
1940
  // final turn's background check still runs). If an asyncRewake check