chainlesschain 0.162.188 → 0.162.193

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 (337) hide show
  1. package/README.md +61 -11
  2. package/bin/chainlesschain.js +7 -52
  3. package/completions/_cc +209 -0
  4. package/completions/cc.bash +18 -0
  5. package/completions/cc.fish +13 -0
  6. package/completions/cc.ps1 +226 -0
  7. package/package.json +10 -2
  8. package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
  9. package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
  10. package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
  11. package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
  12. package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
  13. package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
  14. package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
  15. package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
  16. package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
  17. package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
  18. package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
  19. package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
  20. package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
  21. package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
  22. package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
  23. package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
  24. package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
  25. package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
  26. package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
  27. package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
  28. package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
  29. package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
  30. package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
  31. package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
  32. package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
  33. package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
  34. package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
  35. package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
  37. package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
  38. package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
  39. package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
  40. package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
  41. package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
  42. package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
  43. package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
  44. package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
  45. package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
  46. package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
  47. package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
  48. package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
  49. package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
  50. package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
  51. package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
  52. package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
  53. package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
  54. package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
  55. package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
  56. package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
  57. package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
  58. package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
  59. package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
  60. package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
  61. package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
  62. package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
  63. package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
  64. package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
  66. package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
  67. package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
  68. package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
  69. package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
  70. package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
  71. package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
  72. package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
  73. package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
  74. package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
  75. package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
  76. package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
  77. package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
  78. package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
  79. package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
  80. package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
  81. package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
  82. package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
  83. package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
  84. package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
  85. package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
  86. package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
  88. package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
  89. package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
  90. package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
  91. package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
  92. package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
  93. package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
  94. package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
  95. package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
  96. package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
  97. package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
  98. package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
  99. package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
  127. package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
  133. package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
  134. package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
  135. package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
  138. package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
  139. package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
  140. package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
  141. package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
  142. package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
  143. package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
  144. package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
  145. package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
  146. package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
  147. package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
  148. package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
  149. package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
  151. package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
  153. package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
  154. package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
  155. package/src/assets/web-panel/index.html +1 -1
  156. package/src/cli-help.js +181 -0
  157. package/src/command-help-index.json +181 -0
  158. package/src/command-manifest.json +1107 -175
  159. package/src/command-surface-policy.js +197 -0
  160. package/src/commands/agent.js +98 -33
  161. package/src/commands/artifacts.js +400 -2
  162. package/src/commands/background-session.js +41 -3
  163. package/src/commands/checkpoint-managed.js +1059 -0
  164. package/src/commands/checkpoint-restore-recovery.js +1476 -0
  165. package/src/commands/checkpoint.js +950 -22
  166. package/src/commands/compact.js +101 -25
  167. package/src/commands/config.js +506 -20
  168. package/src/commands/doctor.js +4 -1
  169. package/src/commands/hub.js +1 -1
  170. package/src/commands/mcp.js +688 -24
  171. package/src/commands/pack.js +16 -2
  172. package/src/commands/plugin.js +20 -12
  173. package/src/commands/session-mcp-recovery.js +218 -0
  174. package/src/commands/session.js +470 -110
  175. package/src/commands/setup.js +36 -10
  176. package/src/commands/skill.js +181 -110
  177. package/src/commands/status.js +21 -5
  178. package/src/commands/team-distributed.js +3324 -0
  179. package/src/commands/team.js +1130 -74
  180. package/src/commands/update.js +68 -6
  181. package/src/constants.js +11 -0
  182. package/src/data/changelog.json +26 -0
  183. package/src/gateways/ws/action-protocol.js +10 -0
  184. package/src/gateways/ws/session-protocol.js +555 -27
  185. package/src/gateways/ws/ws-agent-handler.js +541 -14
  186. package/src/gateways/ws/ws-session-gateway.js +129 -22
  187. package/src/gateways/ws/ws-session-state.js +431 -0
  188. package/src/harness/background-task-manager.js +233 -36
  189. package/src/harness/jsonl-session-store.js +2551 -167
  190. package/src/harness/mcp-client.js +802 -61
  191. package/src/harness/prompt-compressor.js +227 -27
  192. package/src/harness/session-index.js +89 -38
  193. package/src/harness/session-list-index.js +254 -0
  194. package/src/harness/structured-handoff.js +410 -0
  195. package/src/lazy-dispatch.js +747 -84
  196. package/src/lib/__tests__/logger.test.js +50 -2
  197. package/src/lib/advisor-runtime.js +1047 -0
  198. package/src/lib/agent-core.js +2 -0
  199. package/src/lib/agent-sandbox.js +120 -26
  200. package/src/lib/agent-session-export.js +20 -0
  201. package/src/lib/agent-team/task-lease.js +271 -3
  202. package/src/lib/agent-team/team-adjudication.js +1501 -0
  203. package/src/lib/agent-team/team-budget.js +4 -1
  204. package/src/lib/agent-team/team-control-store.js +582 -0
  205. package/src/lib/agent-team/team-distributed-queue.js +5246 -0
  206. package/src/lib/agent-team/team-process-checkpoint.js +211 -0
  207. package/src/lib/agent-team/team-runner.js +543 -24
  208. package/src/lib/agent-team/team-worktree.js +1403 -95
  209. package/src/lib/artifact-store.js +131 -8
  210. package/src/lib/background-agent-supervisor.js +185 -13
  211. package/src/lib/background-interaction-journal.js +37 -9
  212. package/src/lib/background-launch-profile.js +1058 -0
  213. package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
  214. package/src/lib/checkpoint-restore-orchestrator.js +987 -0
  215. package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
  216. package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
  217. package/src/lib/checkpoint-restore-recovery.js +884 -0
  218. package/src/lib/checkpoint-restore-retention.js +126 -0
  219. package/src/lib/checkpoint-restore-saga.js +5079 -0
  220. package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
  221. package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
  222. package/src/lib/checkpoint-store.js +1688 -97
  223. package/src/lib/checkpoint-timeline-authority.js +522 -0
  224. package/src/lib/checkpoint-timeline.js +642 -0
  225. package/src/lib/config-keys.js +18 -119
  226. package/src/lib/config-manager.js +446 -27
  227. package/src/lib/config-redaction.js +57 -0
  228. package/src/lib/config-schema.js +853 -0
  229. package/src/lib/cowork-mcp-tools.js +5 -0
  230. package/src/lib/cowork-task-runner.js +458 -63
  231. package/src/lib/delivery-coordinator.js +1234 -0
  232. package/src/lib/delivery-evidence.js +450 -0
  233. package/src/lib/doctor-checkup.js +199 -14
  234. package/src/lib/executable-path.js +34 -0
  235. package/src/lib/fatal-handler.js +16 -0
  236. package/src/lib/file-checkpoint.js +4909 -186
  237. package/src/lib/file-lines.js +125 -0
  238. package/src/lib/headless-config-command.js +16 -5
  239. package/src/lib/hook-runner.cjs +139 -38
  240. package/src/lib/hooks-v2-workspace-context.js +10 -0
  241. package/src/lib/impacted-gate-selector.js +424 -0
  242. package/src/lib/interaction-adapter.js +62 -0
  243. package/src/lib/jsonl-session-store.js +1 -35
  244. package/src/lib/logger.js +66 -14
  245. package/src/lib/managed-checkpoint-render.js +60 -0
  246. package/src/lib/managed-tool-checkpoint.js +194 -0
  247. package/src/lib/mcp-call-ledger-store.js +1069 -0
  248. package/src/lib/mcp-call-ledger.js +747 -0
  249. package/src/lib/mcp-client.js +5 -0
  250. package/src/lib/mcp-conflict-scheduler.js +468 -0
  251. package/src/lib/mcp-effect-contract.js +102 -0
  252. package/src/lib/mcp-headers-helper-trust.js +145 -0
  253. package/src/lib/mcp-headers-helper.js +616 -0
  254. package/src/lib/mcp-host-recovery-runtime.js +131 -0
  255. package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
  256. package/src/lib/mcp-recovery-adjudication.js +443 -0
  257. package/src/lib/output-context.js +86 -0
  258. package/src/lib/packer/file-identity.js +170 -0
  259. package/src/lib/packer/index.js +3 -0
  260. package/src/lib/packer/native-update-state.js +277 -0
  261. package/src/lib/packer/pack-update-applier.js +1983 -136
  262. package/src/lib/packer/pack-update-checker.js +34 -0
  263. package/src/lib/packer/pack-update-downloader.js +875 -86
  264. package/src/lib/packer/pack-update-signature.js +86 -0
  265. package/src/lib/packer/pkg-config-generator.js +6 -0
  266. package/src/lib/paths.js +65 -10
  267. package/src/lib/plan-mode.js +831 -14
  268. package/src/lib/plan-persistence.js +540 -0
  269. package/src/lib/plugin-runtime/hooks.js +227 -69
  270. package/src/lib/plugin-runtime/plugin-options.js +189 -44
  271. package/src/lib/pr-automation-policy.js +146 -8
  272. package/src/lib/process-execution-broker/credential-agent.js +7 -0
  273. package/src/lib/process-execution-broker/index.js +491 -13
  274. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
  275. package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
  276. package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
  277. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  278. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  279. package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
  280. package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
  281. package/src/lib/project-instructions.js +165 -29
  282. package/src/lib/project-mcp-trust.js +67 -0
  283. package/src/lib/public-ide-capability-manifest.js +272 -0
  284. package/src/lib/public-ide-capability-validation.js +351 -0
  285. package/src/lib/secret-store.js +267 -43
  286. package/src/lib/secure-file-identity.js +433 -0
  287. package/src/lib/secure-fs.js +1089 -0
  288. package/src/lib/security-audit.js +58 -0
  289. package/src/lib/service-manager.js +34 -13
  290. package/src/lib/session-budget-runtime.js +1452 -0
  291. package/src/lib/session-host-snapshot.js +388 -0
  292. package/src/lib/session-message-provenance.js +429 -0
  293. package/src/lib/session-projection.js +804 -0
  294. package/src/lib/session-resource-budget.js +1495 -0
  295. package/src/lib/settings-hooks.cjs +138 -22
  296. package/src/lib/settings-loader.cjs +25 -2
  297. package/src/lib/side-effect-ledger-store.js +45 -23
  298. package/src/lib/skill-execution-identity.js +217 -0
  299. package/src/lib/skill-loader.js +299 -18
  300. package/src/lib/sub-agent-context.js +236 -28
  301. package/src/lib/sub-agent-registry.js +5 -0
  302. package/src/lib/subagent-contract.js +94 -0
  303. package/src/lib/todo-manager.js +185 -18
  304. package/src/lib/todo-persistence.js +417 -0
  305. package/src/lib/turn-binding-store.js +41 -23
  306. package/src/lib/turn-binding.js +160 -2
  307. package/src/lib/with-file-lock.js +165 -24
  308. package/src/lib/ws-chat-handler.js +21 -0
  309. package/src/program-base.js +18 -0
  310. package/src/repl/advisor-command.js +103 -0
  311. package/src/repl/agent-repl.js +2175 -146
  312. package/src/repl/btw-command.js +93 -10
  313. package/src/repl/clipboard-image.js +119 -0
  314. package/src/repl/config-summary.js +64 -108
  315. package/src/repl/prompt-editor.js +233 -0
  316. package/src/repl/prompt-interactions.js +344 -0
  317. package/src/repl/prompt-stash.js +354 -0
  318. package/src/repl/prompt-suggestions.js +312 -0
  319. package/src/repl/repl-keybindings.js +227 -0
  320. package/src/repl/session-recap.js +217 -0
  321. package/src/repl/slash-command-registry.js +22 -0
  322. package/src/repl/terminal-layout.js +88 -0
  323. package/src/runtime/agent-core.js +1630 -447
  324. package/src/runtime/coding-agent-contract-shared.cjs +13 -1
  325. package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
  326. package/src/runtime/diagnostics.js +32 -13
  327. package/src/runtime/headless-runner.js +310 -118
  328. package/src/runtime/headless-stream.js +356 -95
  329. package/src/runtime/mcp-config.js +148 -14
  330. package/src/runtime/policies/agent-policy.js +14 -0
  331. package/src/runtime/status-diagnostics-lite.js +137 -0
  332. package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
  333. package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
  334. package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
  335. package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
  336. package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
  337. package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
@@ -22,6 +22,8 @@ import chalk from "chalk";
22
22
  import fs from "fs";
23
23
  import os from "os";
24
24
  import path from "path";
25
+ import { randomUUID } from "node:crypto";
26
+ import { isPromise, isProxy } from "node:util/types";
25
27
  import { logger } from "../lib/logger.js";
26
28
  import { getPlanModeManager, PlanState } from "../lib/plan-mode.js";
27
29
  import { createVimState, feedNormalKey } from "../lib/repl-vim.js";
@@ -45,15 +47,35 @@ import {
45
47
  startSession as jsonlStartSession,
46
48
  appendUserMessage,
47
49
  appendAssistantMessage,
48
- appendCompactEvent,
50
+ appendCompactEventIfMessagesMatch,
49
51
  appendTokenUsage,
50
52
  appendToolCallCompact,
51
53
  appendLlmRetryCompact,
52
54
  appendEvent,
53
- rebuildMessages,
55
+ appendAuthorityEvent,
56
+ isUnsafeSessionId,
54
57
  sessionExists,
58
+ forkSession,
55
59
  } from "../harness/jsonl-session-store.js";
56
60
  import { classifyStreamRetryReason } from "../lib/stream-retry.js";
61
+ import {
62
+ createSessionMcpLedgerSink,
63
+ formatMcpLedgerRecoveryNotice,
64
+ loadMcpLedgerRecovery,
65
+ } from "../lib/mcp-call-ledger-store.js";
66
+ import { createMcpHostRecoveryRuntime } from "../lib/mcp-host-recovery-runtime.js";
67
+ import {
68
+ isVerifiedSessionHostSnapshot,
69
+ readSessionHostResumeState,
70
+ } from "../lib/session-host-snapshot.js";
71
+ import {
72
+ preserveDurableSystemMessageProvenance,
73
+ projectCanonicalResumeMessages,
74
+ } from "../lib/session-message-provenance.js";
75
+ import {
76
+ MCP_RECOVERY_INVALID_CODE,
77
+ classifyMcpRecoveryAdmission,
78
+ } from "../lib/mcp-ledger-recovery-admission.js";
57
79
  import { CLISkillLoader } from "../lib/skill-loader.js";
58
80
  import { storeMemory, consolidateMemory } from "../lib/hierarchical-memory.js";
59
81
  import { CLIContextEngineering } from "../lib/cli-context-engineering.js";
@@ -87,10 +109,11 @@ import {
87
109
  import { HookEvents } from "../lib/hook-manager.js";
88
110
  import { IterationBudget } from "../lib/iteration-budget.js";
89
111
  import { resolveAgentMcp } from "../runtime/mcp-config.js";
90
- import { collapseConsecutiveMessagesInPlace } from "../runtime/message-roles.js";
112
+ import { mergeConsecutiveMessages } from "../runtime/message-roles.js";
91
113
  import {
92
114
  AGENT_TOOLS,
93
115
  buildSystemPrompt,
116
+ chatWithTools,
94
117
  executeTool as coreExecuteTool,
95
118
  agentLoop as coreAgentLoop,
96
119
  formatToolArgs,
@@ -111,8 +134,15 @@ import { resolveSlashMacro } from "./slash-macro.js";
111
134
  import { expandMcpPrompt, renderMcpSurface } from "./mcp-prompt.js";
112
135
  import { newCostStore, addUsage } from "./session-cost.js";
113
136
  import { extractPluginUsageAttribution } from "../lib/plugin-usage-attribution.js";
137
+ import { formatManagedCheckpointEvent } from "../lib/managed-checkpoint-render.js";
114
138
  import { parseThinkCommand, parseEffortCommand } from "./think-command.js";
115
- import { parseBtwCommand, buildAsideBlock, applyAside } from "./btw-command.js";
139
+ import {
140
+ parseBtwCommand,
141
+ parseNoteNextCommand,
142
+ runBtwQuestion,
143
+ buildAsideBlock,
144
+ applyAside,
145
+ } from "./btw-command.js";
116
146
  import { shouldStreamLive } from "./stream-decision.js";
117
147
  import { emptyTurnNotice } from "./empty-turn-notice.js";
118
148
  import {
@@ -295,11 +325,11 @@ export async function agentLoop(messages, options) {
295
325
  // IN PLACE: `coreAgentLoop` mutates this same array (appending assistant/tool
296
326
  // turns), and the REPL reuses it across turns, so folding a copy would leave
297
327
  // the degenerate `[user, user]` pair in the persistent history and re-break
298
- // the next turn. Safe in place because the gated turn is still tool-free (a
299
- // resumed transcript holds only user/assistant/system) and the helper never
300
- // folds `tool` turns regardless.
328
+ // the next turn. The tail helper validates the complete raw transcript first
329
+ // and folds only an exact plain `{role, content}` pair, so recovery authority
330
+ // can never disappear during this live one-shot normalization.
301
331
  if (options.mergeRoles) {
302
- collapseConsecutiveMessagesInPlace(messages);
332
+ collapseValidatedPlainReplTailInPlace(messages);
303
333
  }
304
334
  const usageEvents = [];
305
335
  // Visible cross-vendor fallback notice: a silent switch from the configured
@@ -340,7 +370,14 @@ export async function agentLoop(messages, options) {
340
370
  /* advisory — never break the turn */
341
371
  }
342
372
  }
343
- if (event.type === "checkpoint") {
373
+ const managedCheckpointLine = formatManagedCheckpointEvent(event);
374
+ if (managedCheckpointLine) {
375
+ const render =
376
+ event.type === "managed-checkpoint-error" || event.coverage === "none"
377
+ ? chalk.yellow
378
+ : chalk.gray;
379
+ process.stdout.write(render(`${managedCheckpointLine}\n`));
380
+ } else if (event.type === "checkpoint") {
344
381
  // Remember which file snapshot lines up with the live conversation so
345
382
  // `/rewind <n>` can restore code + conversation together (Claude-Code
346
383
  // parity). atMessageCount = messages.length at snapshot time; see
@@ -471,20 +508,1562 @@ export async function agentLoop(messages, options) {
471
508
  return { content: event.content, usageEvents, thinking: event.thinking };
472
509
  }
473
510
  }
474
- return { content: "", usageEvents };
511
+ return { content: "", usageEvents };
512
+ }
513
+
514
+ const REPL_MCP_RECOVERY_CANDIDATES = new WeakSet();
515
+ const REPL_JSONL_RESUME_CANDIDATES = new WeakSet();
516
+ const REPL_JSONL_RESUME_ABSENCE_CANDIDATES = new WeakSet();
517
+ const REPL_NATIVE_PROMISE = Promise;
518
+ const REPL_NATIVE_PROMISE_PROTOTYPE = Promise.prototype;
519
+ const REPL_NATIVE_PROMISE_THEN = Promise.prototype.then;
520
+ const REPL_NATIVE_PROMISE_CONSTRUCTOR_DESCRIPTOR = Object.freeze({
521
+ ...Object.getOwnPropertyDescriptor(Promise.prototype, "constructor"),
522
+ });
523
+ const REPL_NATIVE_PROMISE_SPECIES_DESCRIPTOR = Object.freeze({
524
+ ...Object.getOwnPropertyDescriptor(Promise, Symbol.species),
525
+ });
526
+ const REPL_SAFE_PROMISE_CONSTRUCTOR = Object.freeze(
527
+ Object.defineProperty(Object.create(null), Symbol.species, {
528
+ configurable: false,
529
+ enumerable: false,
530
+ writable: false,
531
+ value: null,
532
+ }),
533
+ );
534
+ const REPL_NATIVE_PROMISE_REJECTION_HANDLER = () => {};
535
+ const REPL_PAYLOAD_DIGEST = /^sha256:[0-9a-f]{64}$/;
536
+ const REPL_AUTHORITY_HASH = /^[0-9a-f]{64}$/;
537
+ const REPL_RESUME_SESSION_ID_MAX_BYTES = 128;
538
+ const REPL_RESUME_SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
539
+ const REPL_WINDOWS_DEVICE_NAME =
540
+ /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
541
+ const REPL_RESUME_ROLES = new Set(["system", "user", "assistant", "tool"]);
542
+ const REPL_MCP_EFFECTS = new Set(["read", "unknown", "write", "destructive"]);
543
+ const REPL_REPLAY_DENY_FIELDS = new Set([
544
+ "ledgerId",
545
+ "serverName",
546
+ "toolName",
547
+ "inputBytes",
548
+ "replayDigest",
549
+ ]);
550
+ const REPL_MCP_REMEDIATIONS = new Set([
551
+ "inspect_transcript",
552
+ "adjudicate_started_calls",
553
+ "exact_replay_denied",
554
+ ]);
555
+
556
+ function invalidReplRecovery(message, code = MCP_RECOVERY_INVALID_CODE) {
557
+ const error = new TypeError(message);
558
+ error.code = code;
559
+ return error;
560
+ }
561
+
562
+ function validateReplResumeSessionId(sessionId) {
563
+ if (
564
+ isUnsafeSessionId(sessionId) ||
565
+ Buffer.byteLength(sessionId, "utf8") > REPL_RESUME_SESSION_ID_MAX_BYTES ||
566
+ !REPL_RESUME_SESSION_ID_PATTERN.test(sessionId) ||
567
+ REPL_WINDOWS_DEVICE_NAME.test(sessionId) ||
568
+ sessionId.endsWith(".") ||
569
+ sessionId.normalize("NFC") !== sessionId
570
+ ) {
571
+ throw invalidReplRecovery(
572
+ "REPL resume session id is invalid",
573
+ "CC_REPL_SESSION_ID_INVALID",
574
+ );
575
+ }
576
+ return sessionId;
577
+ }
578
+
579
+ function brandedFrozenCandidate(brand, value) {
580
+ const candidate = Object.freeze(value);
581
+ brand.add(candidate);
582
+ return candidate;
583
+ }
584
+
585
+ function safeRecoveryErrorValue(error, key, fallback) {
586
+ try {
587
+ if (error && !isProxy(error)) {
588
+ const descriptor = Object.getOwnPropertyDescriptor(error, key);
589
+ if (descriptor && "value" in descriptor) return descriptor.value;
590
+ }
591
+ } catch {
592
+ // Malformed errors are diagnostic input, never recovery authority.
593
+ }
594
+ return fallback;
595
+ }
596
+
597
+ function replMcpRecoveryError(error, fallbackCode) {
598
+ const message = safeRecoveryErrorValue(
599
+ error,
600
+ "message",
601
+ "REPL session recovery failed",
602
+ );
603
+ let cause = new Error("REPL session recovery failed");
604
+ try {
605
+ if (!isProxy(error) && error instanceof Error) cause = error;
606
+ } catch {
607
+ // Keep the inert fallback cause for hostile diagnostic objects.
608
+ }
609
+ const wrapped = new Error(
610
+ typeof message === "string" && message
611
+ ? message
612
+ : "REPL session recovery failed",
613
+ { cause },
614
+ );
615
+ const code = safeRecoveryErrorValue(error, "code", fallbackCode);
616
+ wrapped.code = typeof code === "string" && code ? code : fallbackCode;
617
+ return wrapped;
618
+ }
619
+
620
+ function samePropertyDescriptor(actual, expected) {
621
+ if (!actual || !expected) return actual === expected;
622
+ if (
623
+ actual.configurable !== expected.configurable ||
624
+ actual.enumerable !== expected.enumerable
625
+ ) {
626
+ return false;
627
+ }
628
+ if ("value" in expected) {
629
+ return (
630
+ "value" in actual &&
631
+ actual.value === expected.value &&
632
+ actual.writable === expected.writable
633
+ );
634
+ }
635
+ return (
636
+ !("value" in actual) &&
637
+ actual.get === expected.get &&
638
+ actual.set === expected.set
639
+ );
640
+ }
641
+
642
+ function unconsumableNativePromise(subject) {
643
+ return invalidReplRecovery(
644
+ `${subject} is a native Promise with an unsafe non-configurable constructor; ` +
645
+ "its producer must observe the rejection",
646
+ "CC_REPL_NATIVE_PROMISE_UNCONSUMABLE",
647
+ );
648
+ }
649
+
650
+ function consumeNativeReplPromise(value, subject) {
651
+ const prototype = Object.getPrototypeOf(value);
652
+ if (isProxy(prototype)) throw unconsumableNativePromise(subject);
653
+
654
+ const constructorDescriptor = Object.getOwnPropertyDescriptor(
655
+ value,
656
+ "constructor",
657
+ );
658
+ const intrinsicSpeciesIntact = samePropertyDescriptor(
659
+ Object.getOwnPropertyDescriptor(REPL_NATIVE_PROMISE, Symbol.species),
660
+ REPL_NATIVE_PROMISE_SPECIES_DESCRIPTOR,
661
+ );
662
+ const inheritedIntrinsicConstructor =
663
+ !constructorDescriptor &&
664
+ prototype === REPL_NATIVE_PROMISE_PROTOTYPE &&
665
+ samePropertyDescriptor(
666
+ Object.getOwnPropertyDescriptor(
667
+ REPL_NATIVE_PROMISE_PROTOTYPE,
668
+ "constructor",
669
+ ),
670
+ REPL_NATIVE_PROMISE_CONSTRUCTOR_DESCRIPTOR,
671
+ ) &&
672
+ intrinsicSpeciesIntact;
673
+ const ownIntrinsicConstructor =
674
+ constructorDescriptor &&
675
+ "value" in constructorDescriptor &&
676
+ (constructorDescriptor.value === undefined ||
677
+ (constructorDescriptor.value === REPL_NATIVE_PROMISE &&
678
+ intrinsicSpeciesIntact));
679
+
680
+ if (inheritedIntrinsicConstructor || ownIntrinsicConstructor) {
681
+ Reflect.apply(REPL_NATIVE_PROMISE_THEN, value, [
682
+ undefined,
683
+ REPL_NATIVE_PROMISE_REJECTION_HANDLER,
684
+ ]);
685
+ return;
686
+ }
687
+
688
+ const canShadow = constructorDescriptor
689
+ ? constructorDescriptor.configurable === true
690
+ : Object.isExtensible(value);
691
+ if (!canShadow) {
692
+ // Promise.prototype.then necessarily performs SpeciesConstructor, which
693
+ // would execute this hostile constructor/accessor. ECMAScript exposes no
694
+ // separate hook-free operation that marks a rejection handled. Fail closed
695
+ // without pretending the Promise was consumed; the producer must have
696
+ // attached its own rejection observer before returning this invalid value.
697
+ throw unconsumableNativePromise(subject);
698
+ }
699
+
700
+ Object.defineProperty(value, "constructor", {
701
+ configurable: true,
702
+ enumerable: constructorDescriptor?.enumerable ?? false,
703
+ writable: false,
704
+ value: REPL_SAFE_PROMISE_CONSTRUCTOR,
705
+ });
706
+ try {
707
+ Reflect.apply(REPL_NATIVE_PROMISE_THEN, value, [
708
+ undefined,
709
+ REPL_NATIVE_PROMISE_REJECTION_HANDLER,
710
+ ]);
711
+ } finally {
712
+ if (constructorDescriptor) {
713
+ Object.defineProperty(value, "constructor", constructorDescriptor);
714
+ } else {
715
+ Reflect.deleteProperty(value, "constructor");
716
+ }
717
+ }
718
+ }
719
+
720
+ function synchronousReplRecoveryValue(value, subject) {
721
+ if (
722
+ value === null ||
723
+ value === undefined ||
724
+ (typeof value !== "object" && typeof value !== "function")
725
+ ) {
726
+ return value;
727
+ }
728
+ if (isProxy(value)) {
729
+ throw invalidReplRecovery(`${subject} must not be a Proxy`);
730
+ }
731
+
732
+ // Consume a genuine Promise before inspecting any user-controlled `then`
733
+ // descriptor. A Promise can shadow `then` with an accessor or data property;
734
+ // reading that descriptor first would leave an already-rejected Promise
735
+ // unobserved even though the recovery boundary correctly rejected it.
736
+ if (isPromise(value)) {
737
+ consumeNativeReplPromise(value, subject);
738
+ throw invalidReplRecovery(`${subject} must be returned synchronously`);
739
+ }
740
+
741
+ let prototype = value;
742
+ while (prototype) {
743
+ // An ordinary object can hide a Proxy in its prototype chain. Check every
744
+ // level before reflection so no getOwnPropertyDescriptor/getPrototypeOf
745
+ // trap is executed while rejecting untrusted recovery evidence.
746
+ if (isProxy(prototype)) {
747
+ throw invalidReplRecovery(`${subject} must not inherit from a Proxy`);
748
+ }
749
+ const thenDescriptor = Object.getOwnPropertyDescriptor(prototype, "then");
750
+ if (thenDescriptor) {
751
+ if (!("value" in thenDescriptor)) {
752
+ throw invalidReplRecovery(`${subject}.then must not be an accessor`);
753
+ }
754
+ if (typeof thenDescriptor.value === "function") {
755
+ // Never invoke an arbitrary user-supplied thenable capability.
756
+ throw invalidReplRecovery(`${subject} must be returned synchronously`);
757
+ }
758
+ break;
759
+ }
760
+ prototype = Object.getPrototypeOf(prototype);
761
+ }
762
+ return value;
763
+ }
764
+
765
+ function plainDataDescriptors(value, subject) {
766
+ synchronousReplRecoveryValue(value, subject);
767
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
768
+ throw invalidReplRecovery(`${subject} must be a plain data object`);
769
+ }
770
+ const prototype = Object.getPrototypeOf(value);
771
+ if (prototype !== Object.prototype && prototype !== null) {
772
+ throw invalidReplRecovery(`${subject} must have a plain prototype`);
773
+ }
774
+ const descriptors = Object.getOwnPropertyDescriptors(value);
775
+ for (const key of Reflect.ownKeys(descriptors)) {
776
+ if (typeof key === "symbol" || !("value" in descriptors[key])) {
777
+ throw invalidReplRecovery(`${subject} contains an accessor or symbol`);
778
+ }
779
+ }
780
+ return descriptors;
781
+ }
782
+
783
+ function dataDescriptorValue(
784
+ descriptors,
785
+ key,
786
+ subject,
787
+ { required = true, fallback = null } = {},
788
+ ) {
789
+ const descriptor = descriptors[key];
790
+ if (!descriptor) {
791
+ if (!required) return fallback;
792
+ throw invalidReplRecovery(`${subject}.${key} is missing`);
793
+ }
794
+ if (!("value" in descriptor)) {
795
+ throw invalidReplRecovery(`${subject}.${key} must be a data property`);
796
+ }
797
+ return descriptor.value;
798
+ }
799
+
800
+ function snapshotDataArray(value, subject, mapper) {
801
+ synchronousReplRecoveryValue(value, subject);
802
+ if (!Array.isArray(value) || isProxy(value)) {
803
+ throw invalidReplRecovery(`${subject} must be a non-Proxy array`);
804
+ }
805
+ const descriptors = Object.getOwnPropertyDescriptors(value);
806
+ for (const key of Reflect.ownKeys(descriptors)) {
807
+ if (typeof key === "symbol" || !("value" in descriptors[key])) {
808
+ throw invalidReplRecovery(`${subject} contains an accessor or symbol`);
809
+ }
810
+ }
811
+ const length = descriptors.length?.value;
812
+ if (!Number.isSafeInteger(length) || length < 0) {
813
+ throw invalidReplRecovery(`${subject}.length is invalid`);
814
+ }
815
+ const snapshots = [];
816
+ for (let index = 0; index < length; index += 1) {
817
+ const descriptor = descriptors[String(index)];
818
+ if (!descriptor || !("value" in descriptor)) {
819
+ throw invalidReplRecovery(`${subject}[${index}] is not plain data`);
820
+ }
821
+ snapshots.push(mapper(descriptor.value, index));
822
+ }
823
+ return Object.freeze(snapshots);
824
+ }
825
+
826
+ function requiredRecoveryString(value, subject, { nullable = false } = {}) {
827
+ if (value === null && nullable) return null;
828
+ if (typeof value !== "string" || (!nullable && !value)) {
829
+ throw invalidReplRecovery(`${subject} must be a string`);
830
+ }
831
+ return value;
832
+ }
833
+
834
+ function snapshotReplMcpRecord(record, index) {
835
+ const subject = `MCP recovery unsettled[${index}]`;
836
+ const descriptors = plainDataDescriptors(record, subject);
837
+ const effectContract = dataDescriptorValue(
838
+ descriptors,
839
+ "effectContract",
840
+ subject,
841
+ );
842
+ const effectDescriptors = plainDataDescriptors(
843
+ effectContract,
844
+ `${subject}.effectContract`,
845
+ );
846
+ const effect = requiredRecoveryString(
847
+ dataDescriptorValue(
848
+ effectDescriptors,
849
+ "effect",
850
+ `${subject}.effectContract`,
851
+ ),
852
+ `${subject}.effectContract.effect`,
853
+ );
854
+ if (!REPL_MCP_EFFECTS.has(effect)) {
855
+ throw invalidReplRecovery(`${subject}.effectContract.effect is invalid`);
856
+ }
857
+ return Object.freeze({
858
+ ledgerId: requiredRecoveryString(
859
+ dataDescriptorValue(descriptors, "ledgerId", subject),
860
+ `${subject}.ledgerId`,
861
+ ),
862
+ serverName: requiredRecoveryString(
863
+ dataDescriptorValue(descriptors, "serverName", subject),
864
+ `${subject}.serverName`,
865
+ ),
866
+ toolName: requiredRecoveryString(
867
+ dataDescriptorValue(descriptors, "toolName", subject),
868
+ `${subject}.toolName`,
869
+ ),
870
+ effectContract: Object.freeze({ effect }),
871
+ });
872
+ }
873
+
874
+ function snapshotReplMcpIncident(incident, index) {
875
+ const subject = `MCP recovery incidents[${index}]`;
876
+ const descriptors = plainDataDescriptors(incident, subject);
877
+ return Object.freeze({
878
+ code: requiredRecoveryString(
879
+ dataDescriptorValue(descriptors, "code", subject),
880
+ `${subject}.code`,
881
+ ),
882
+ ledgerId: requiredRecoveryString(
883
+ dataDescriptorValue(descriptors, "ledgerId", subject, {
884
+ required: false,
885
+ fallback: null,
886
+ }),
887
+ `${subject}.ledgerId`,
888
+ { nullable: true },
889
+ ),
890
+ });
891
+ }
892
+
893
+ function snapshotReplReplayDeny(entry, index) {
894
+ const subject = `MCP recovery replayDenied[${index}]`;
895
+ const descriptors = plainDataDescriptors(entry, subject);
896
+ const fields = Object.keys(descriptors);
897
+ if (
898
+ fields.length !== REPL_REPLAY_DENY_FIELDS.size ||
899
+ fields.some((field) => !REPL_REPLAY_DENY_FIELDS.has(field))
900
+ ) {
901
+ throw invalidReplRecovery(`${subject} has an invalid exact schema`);
902
+ }
903
+ const snapshot = Object.freeze({
904
+ ledgerId: requiredRecoveryString(
905
+ dataDescriptorValue(descriptors, "ledgerId", subject),
906
+ `${subject}.ledgerId`,
907
+ ),
908
+ serverName: requiredRecoveryString(
909
+ dataDescriptorValue(descriptors, "serverName", subject),
910
+ `${subject}.serverName`,
911
+ ),
912
+ toolName: requiredRecoveryString(
913
+ dataDescriptorValue(descriptors, "toolName", subject),
914
+ `${subject}.toolName`,
915
+ ),
916
+ inputBytes: dataDescriptorValue(descriptors, "inputBytes", subject),
917
+ replayDigest: requiredRecoveryString(
918
+ dataDescriptorValue(descriptors, "replayDigest", subject),
919
+ `${subject}.replayDigest`,
920
+ ),
921
+ });
922
+ if (
923
+ !Number.isInteger(snapshot.inputBytes) ||
924
+ snapshot.inputBytes < 0 ||
925
+ !REPL_PAYLOAD_DIGEST.test(snapshot.replayDigest)
926
+ ) {
927
+ throw invalidReplRecovery(`${subject} has an invalid exact identity`);
928
+ }
929
+ return snapshot;
930
+ }
931
+
932
+ function strictReplMcpRecoverySnapshot(recovery, sessionId) {
933
+ const descriptors = plainDataDescriptors(recovery, "MCP recovery");
934
+ const recoverySessionId = requiredRecoveryString(
935
+ dataDescriptorValue(descriptors, "sessionId", "MCP recovery"),
936
+ "MCP recovery.sessionId",
937
+ );
938
+ if (recoverySessionId !== sessionId) {
939
+ throw invalidReplRecovery("MCP recovery session does not match the target");
940
+ }
941
+ if (dataDescriptorValue(descriptors, "verified", "MCP recovery") !== true) {
942
+ throw invalidReplRecovery("MCP recovery was not verified");
943
+ }
944
+ const unsettled = snapshotDataArray(
945
+ dataDescriptorValue(descriptors, "unsettled", "MCP recovery"),
946
+ "MCP recovery.unsettled",
947
+ snapshotReplMcpRecord,
948
+ );
949
+ const incidents = snapshotDataArray(
950
+ dataDescriptorValue(descriptors, "incidents", "MCP recovery"),
951
+ "MCP recovery.incidents",
952
+ snapshotReplMcpIncident,
953
+ );
954
+ // Exact-replay denies are authority, not presentation samples. Preserve and
955
+ // freeze every entry: truncation would silently re-enable a denied call.
956
+ const replayDenied = snapshotDataArray(
957
+ dataDescriptorValue(descriptors, "replayDenied", "MCP recovery"),
958
+ "MCP recovery.replayDenied",
959
+ snapshotReplReplayDeny,
960
+ );
961
+ const headHash = requiredRecoveryString(
962
+ dataDescriptorValue(descriptors, "headHash", "MCP recovery"),
963
+ "MCP recovery.headHash",
964
+ { nullable: true },
965
+ );
966
+ const recoveryDigest = requiredRecoveryString(
967
+ dataDescriptorValue(descriptors, "recoveryDigest", "MCP recovery"),
968
+ "MCP recovery.recoveryDigest",
969
+ );
970
+ const remediation = requiredRecoveryString(
971
+ dataDescriptorValue(descriptors, "remediation", "MCP recovery"),
972
+ "MCP recovery.remediation",
973
+ { nullable: true },
974
+ );
975
+ if (
976
+ (headHash !== null && !REPL_AUTHORITY_HASH.test(headHash)) ||
977
+ !REPL_PAYLOAD_DIGEST.test(recoveryDigest) ||
978
+ (remediation !== null && !REPL_MCP_REMEDIATIONS.has(remediation))
979
+ ) {
980
+ throw invalidReplRecovery("MCP recovery authority metadata is invalid");
981
+ }
982
+ const expectedRemediation =
983
+ incidents.length > 0
984
+ ? "inspect_transcript"
985
+ : unsettled.length > 0
986
+ ? "adjudicate_started_calls"
987
+ : replayDenied.length > 0
988
+ ? "exact_replay_denied"
989
+ : null;
990
+ if (remediation !== expectedRemediation) {
991
+ throw invalidReplRecovery("MCP recovery remediation is inconsistent");
992
+ }
993
+
994
+ const snapshot = Object.freeze({
995
+ sessionId: recoverySessionId,
996
+ verified: true,
997
+ unsettled,
998
+ incidents,
999
+ replayDenied,
1000
+ headHash,
1001
+ recoveryDigest,
1002
+ remediation,
1003
+ });
1004
+ const admission = classifyMcpRecoveryAdmission(snapshot);
1005
+ if (admission.reasonCode) {
1006
+ throw invalidReplRecovery(
1007
+ "MCP recovery projection is malformed",
1008
+ admission.reasonCode,
1009
+ );
1010
+ }
1011
+ return snapshot;
1012
+ }
1013
+
1014
+ function dependencyDataFunction(dependencies, key, fallback) {
1015
+ if (dependencies == null) return fallback;
1016
+ if (typeof dependencies !== "object" || isProxy(dependencies)) {
1017
+ throw invalidReplRecovery("REPL recovery dependencies are invalid");
1018
+ }
1019
+ const descriptor = Object.getOwnPropertyDescriptor(dependencies, key);
1020
+ if (!descriptor) return fallback;
1021
+ if (!("value" in descriptor) || typeof descriptor.value !== "function") {
1022
+ throw invalidReplRecovery(`REPL recovery dependency ${key} is invalid`);
1023
+ }
1024
+ return descriptor.value;
1025
+ }
1026
+
1027
+ function replMcpRecoveryFailureNotice(error, subject = "MCP call ledger") {
1028
+ return (
1029
+ `MCP recovery notice — the durable ${subject} could not be verified. ` +
1030
+ "Do not execute or retry MCP tools until the session transcript has " +
1031
+ `been inspected (${error?.code || "CC_MCP_LEDGER_EVENT_READ_FAILED"}).`
1032
+ );
1033
+ }
1034
+
1035
+ function failClosedReplMcpRecoveryCandidate(
1036
+ sessionId,
1037
+ error,
1038
+ fallbackCode = "CC_MCP_LEDGER_EVENT_READ_FAILED",
1039
+ subject = "MCP call ledger",
1040
+ ) {
1041
+ const recoveryError = replMcpRecoveryError(error, fallbackCode);
1042
+ return brandedFrozenCandidate(REPL_MCP_RECOVERY_CANDIDATES, {
1043
+ sessionId,
1044
+ recovery: null,
1045
+ recoveryError,
1046
+ notice: replMcpRecoveryFailureNotice(recoveryError, subject),
1047
+ });
1048
+ }
1049
+
1050
+ /**
1051
+ * Read one target session's verified MCP recovery projection without mutating
1052
+ * the active REPL. Callers commit the returned candidate only after the rest
1053
+ * of a session switch (notably message rebuild) has succeeded.
1054
+ */
1055
+ export function readReplMcpRecoveryCandidate(sessionId, dependencies = {}) {
1056
+ try {
1057
+ const loadRecovery = dependencyDataFunction(
1058
+ dependencies,
1059
+ "loadMcpLedgerRecovery",
1060
+ loadMcpLedgerRecovery,
1061
+ );
1062
+ const formatNotice = dependencyDataFunction(
1063
+ dependencies,
1064
+ "formatMcpLedgerRecoveryNotice",
1065
+ formatMcpLedgerRecoveryNotice,
1066
+ );
1067
+ return createReplMcpRecoveryCandidate(
1068
+ sessionId,
1069
+ synchronousReplRecoveryValue(
1070
+ loadRecovery(sessionId),
1071
+ "MCP recovery loader result",
1072
+ ),
1073
+ formatNotice,
1074
+ );
1075
+ } catch (error) {
1076
+ return failClosedReplMcpRecoveryCandidate(sessionId, error);
1077
+ }
1078
+ }
1079
+
1080
+ function createReplMcpRecoveryCandidate(
1081
+ sessionId,
1082
+ recoveryValue,
1083
+ formatNotice = formatMcpLedgerRecoveryNotice,
1084
+ ) {
1085
+ try {
1086
+ const recovery = strictReplMcpRecoverySnapshot(recoveryValue, sessionId);
1087
+ let notice = synchronousReplRecoveryValue(
1088
+ formatNotice(recovery),
1089
+ "MCP recovery formatter result",
1090
+ );
1091
+ if (notice !== null && typeof notice !== "string") {
1092
+ throw invalidReplRecovery(
1093
+ "MCP recovery formatter must return a string or null",
1094
+ );
1095
+ }
1096
+ if (
1097
+ !notice &&
1098
+ (recovery.unsettled.length > 0 ||
1099
+ recovery.incidents.length > 0 ||
1100
+ recovery.replayDenied.length > 0)
1101
+ ) {
1102
+ notice =
1103
+ "MCP recovery notice — interrupted MCP calls, ledger incidents, " +
1104
+ "or exact replay denies require explicit inspection before replay.";
1105
+ }
1106
+ return brandedFrozenCandidate(REPL_MCP_RECOVERY_CANDIDATES, {
1107
+ sessionId,
1108
+ recovery,
1109
+ recoveryError: null,
1110
+ notice: notice || null,
1111
+ });
1112
+ } catch (error) {
1113
+ return failClosedReplMcpRecoveryCandidate(sessionId, error);
1114
+ }
1115
+ }
1116
+
1117
+ function snapshotReplMessageValue(value, subject, ancestors = new WeakSet()) {
1118
+ if (
1119
+ value === null ||
1120
+ value === undefined ||
1121
+ typeof value === "string" ||
1122
+ typeof value === "boolean" ||
1123
+ (typeof value === "number" && Number.isFinite(value))
1124
+ ) {
1125
+ return value;
1126
+ }
1127
+ if (!value || typeof value !== "object" || isProxy(value)) {
1128
+ throw invalidReplRecovery(
1129
+ `${subject} contains a non-serializable value`,
1130
+ "CC_REPL_SESSION_REBUILD_FAILED",
1131
+ );
1132
+ }
1133
+ if (ancestors.has(value)) {
1134
+ throw invalidReplRecovery(
1135
+ `${subject} contains a cycle`,
1136
+ "CC_REPL_SESSION_REBUILD_FAILED",
1137
+ );
1138
+ }
1139
+ ancestors.add(value);
1140
+ try {
1141
+ if (Array.isArray(value)) {
1142
+ return snapshotDataArray(value, subject, (item, index) =>
1143
+ snapshotReplMessageValue(item, `${subject}[${index}]`, ancestors),
1144
+ );
1145
+ }
1146
+ const descriptors = plainDataDescriptors(value, subject);
1147
+ const snapshot = {};
1148
+ for (const key of Object.keys(descriptors)) {
1149
+ snapshot[key] = snapshotReplMessageValue(
1150
+ descriptors[key].value,
1151
+ `${subject}.${key}`,
1152
+ ancestors,
1153
+ );
1154
+ }
1155
+ return Object.freeze(snapshot);
1156
+ } finally {
1157
+ ancestors.delete(value);
1158
+ }
1159
+ }
1160
+
1161
+ function snapshotReplMessages(messages) {
1162
+ try {
1163
+ return snapshotDataArray(
1164
+ messages,
1165
+ "REPL session rebuild",
1166
+ (message, index) => {
1167
+ const snapshot = snapshotReplMessageValue(
1168
+ message,
1169
+ `REPL session rebuild[${index}]`,
1170
+ );
1171
+ if (
1172
+ !snapshot ||
1173
+ Array.isArray(snapshot) ||
1174
+ typeof snapshot.role !== "string"
1175
+ ) {
1176
+ throw invalidReplRecovery(
1177
+ `REPL session rebuild[${index}] is not a message`,
1178
+ "CC_REPL_SESSION_REBUILD_FAILED",
1179
+ );
1180
+ }
1181
+ return preserveDurableSystemMessageProvenance(message, snapshot);
1182
+ },
1183
+ );
1184
+ } catch (cause) {
1185
+ const error = invalidReplRecovery(
1186
+ safeRecoveryErrorValue(cause, "message", "REPL session rebuild failed"),
1187
+ "CC_REPL_SESSION_REBUILD_FAILED",
1188
+ );
1189
+ error.cause = cause;
1190
+ throw error;
1191
+ }
1192
+ }
1193
+
1194
+ function canonicalPersistentReplMessages(messages) {
1195
+ return snapshotReplMessages(
1196
+ projectCanonicalResumeMessages(snapshotReplMessages(messages), {
1197
+ strict: true,
1198
+ }),
1199
+ );
1200
+ }
1201
+
1202
+ /**
1203
+ * Track the exact replay projection this REPL has persisted. Compact writes
1204
+ * compare it with the store's verified active messages under one lock, so a
1205
+ * second host's unseen turn can never be hidden by a local auto/session-end
1206
+ * checkpoint.
1207
+ */
1208
+ export function createReplCompactPersistence(
1209
+ initialMessages = [],
1210
+ dependencies = {},
1211
+ ) {
1212
+ const appendCompact = dependencyDataFunction(
1213
+ dependencies,
1214
+ "appendCompactEventIfMessagesMatch",
1215
+ appendCompactEventIfMessagesMatch,
1216
+ );
1217
+ let persistedMessages = canonicalPersistentReplMessages(initialMessages);
1218
+ const replace = (messages) => {
1219
+ persistedMessages = canonicalPersistentReplMessages(messages);
1220
+ return persistedMessages;
1221
+ };
1222
+ return Object.freeze({
1223
+ record(message) {
1224
+ const projected = canonicalPersistentReplMessages([message]);
1225
+ if (projected.length !== 1) {
1226
+ throw new TypeError("Persisted REPL message is not canonical");
1227
+ }
1228
+ persistedMessages = Object.freeze([...persistedMessages, projected[0]]);
1229
+ return projected[0];
1230
+ },
1231
+ replace,
1232
+ persist(sessionId, payload) {
1233
+ const canonicalMessages = canonicalPersistentReplMessages(
1234
+ payload?.messages || [],
1235
+ );
1236
+ const canonicalPayload = { ...payload, messages: canonicalMessages };
1237
+ const result = appendCompact(
1238
+ sessionId,
1239
+ canonicalPayload,
1240
+ persistedMessages,
1241
+ );
1242
+ replace(canonicalMessages);
1243
+ return result;
1244
+ },
1245
+ snapshot() {
1246
+ return snapshotReplMessages(persistedMessages);
1247
+ },
1248
+ });
1249
+ }
1250
+
1251
+ function validateRawReplReplayMessages(rebuiltMessages) {
1252
+ for (let index = 0; index < rebuiltMessages.length; index += 1) {
1253
+ const message = rebuiltMessages[index];
1254
+ if (!REPL_RESUME_ROLES.has(message.role)) {
1255
+ throw invalidReplRecovery(
1256
+ `REPL session message[${index}] has an unsupported role`,
1257
+ "CC_REPL_SESSION_ROLE_INVALID",
1258
+ );
1259
+ }
1260
+
1261
+ const hasToolCalls = Object.prototype.hasOwnProperty.call(
1262
+ message,
1263
+ "tool_calls",
1264
+ );
1265
+ const hasToolCallId = Object.prototype.hasOwnProperty.call(
1266
+ message,
1267
+ "tool_call_id",
1268
+ );
1269
+ if (message.role !== "assistant" && hasToolCalls) {
1270
+ throw invalidReplRecovery(
1271
+ `REPL session message[${index}] carries tool calls on the wrong role`,
1272
+ "CC_REPL_SESSION_TOOL_PAIR_INVALID",
1273
+ );
1274
+ }
1275
+ if (message.role !== "tool" && hasToolCallId) {
1276
+ throw invalidReplRecovery(
1277
+ `REPL session message[${index}] carries a tool result id on the wrong role`,
1278
+ "CC_REPL_SESSION_TOOL_PAIR_INVALID",
1279
+ );
1280
+ }
1281
+
1282
+ if (message.role === "tool") {
1283
+ if (
1284
+ !hasToolCallId ||
1285
+ typeof message.tool_call_id !== "string" ||
1286
+ message.tool_call_id.length === 0
1287
+ ) {
1288
+ throw invalidReplRecovery(
1289
+ `REPL session tool result[${index}] has an invalid call id`,
1290
+ "CC_REPL_SESSION_TOOL_PAIR_INVALID",
1291
+ );
1292
+ }
1293
+ continue;
1294
+ }
1295
+
1296
+ if (message.role !== "assistant" || !hasToolCalls) continue;
1297
+ if (!Array.isArray(message.tool_calls)) {
1298
+ throw invalidReplRecovery(
1299
+ `REPL session assistant message[${index}] has invalid tool calls`,
1300
+ "CC_REPL_SESSION_TOOL_PAIR_INVALID",
1301
+ );
1302
+ }
1303
+ const batchToolCallIds = new Set();
1304
+ for (const toolCall of message.tool_calls) {
1305
+ const toolCallId =
1306
+ toolCall && typeof toolCall === "object" && !Array.isArray(toolCall)
1307
+ ? toolCall.id
1308
+ : null;
1309
+ if (
1310
+ typeof toolCallId !== "string" ||
1311
+ toolCallId.length === 0 ||
1312
+ batchToolCallIds.has(toolCallId)
1313
+ ) {
1314
+ throw invalidReplRecovery(
1315
+ `REPL session assistant message[${index}] has ambiguous tool call ids`,
1316
+ "CC_REPL_SESSION_TOOL_PAIR_INVALID",
1317
+ );
1318
+ }
1319
+ batchToolCallIds.add(toolCallId);
1320
+ }
1321
+ }
1322
+ }
1323
+
1324
+ function isPlainReplMergeMessage(message) {
1325
+ if (message.role !== "user" && message.role !== "assistant") return false;
1326
+ const keys = Object.keys(message);
1327
+ return (
1328
+ Object.prototype.hasOwnProperty.call(message, "content") &&
1329
+ keys.every((key) => key === "role" || key === "content")
1330
+ );
1331
+ }
1332
+
1333
+ function mergePlainReplRoleRuns(rebuiltMessages) {
1334
+ const mergedMessages = [];
1335
+ for (const message of rebuiltMessages) {
1336
+ const previous = mergedMessages.at(-1);
1337
+ if (
1338
+ previous &&
1339
+ previous.role === message.role &&
1340
+ isPlainReplMergeMessage(previous) &&
1341
+ isPlainReplMergeMessage(message)
1342
+ ) {
1343
+ // The shared role helper is safe only after raw authority validation and
1344
+ // only for exact {role, content} records. This prevents a later record's
1345
+ // tool/identity authority from disappearing behind object spread.
1346
+ mergedMessages[mergedMessages.length - 1] = mergeConsecutiveMessages([
1347
+ previous,
1348
+ message,
1349
+ ])[0];
1350
+ } else {
1351
+ mergedMessages.push(message);
1352
+ }
1353
+ }
1354
+ return snapshotReplMessages(mergedMessages);
1355
+ }
1356
+
1357
+ function collapseValidatedPlainReplTailInPlace(messages) {
1358
+ const rawSnapshot = snapshotReplMessages(messages);
1359
+ validateRawReplReplayMessages(rawSnapshot);
1360
+ const previous = rawSnapshot.at(-2);
1361
+ const current = rawSnapshot.at(-1);
1362
+ if (
1363
+ !previous ||
1364
+ previous.role !== current?.role ||
1365
+ !isPlainReplMergeMessage(previous) ||
1366
+ !isPlainReplMergeMessage(current)
1367
+ ) {
1368
+ return false;
1369
+ }
1370
+ const merged = mergeConsecutiveMessages([previous, current]);
1371
+ if (merged.length !== 1) return false;
1372
+ messages.splice(messages.length - 2, 2, merged[0]);
1373
+ return true;
1374
+ }
1375
+
1376
+ function normalizeReplReplayMessages(rebuiltMessages) {
1377
+ // Validate the immutable raw snapshot before any normalization. In
1378
+ // particular, never let a same-role merge hide malformed tool authority.
1379
+ validateRawReplReplayMessages(rebuiltMessages);
1380
+
1381
+ // Stable-partition verified canonical systems ahead of conversation. Their
1382
+ // relative order is preserved exactly, without inspecting content. This
1383
+ // keeps a trailing migration/fork marker from separating a dangling user
1384
+ // turn from the next live user turn after providers extract system prompts.
1385
+ const canonicalSystemMessages = snapshotReplMessages(
1386
+ rebuiltMessages.filter((message) => message.role === "system"),
1387
+ );
1388
+ const conversationMessages = mergePlainReplRoleRuns(
1389
+ rebuiltMessages.filter((message) => message.role !== "system"),
1390
+ );
1391
+ let phase = "expect-initial-conversation-message";
1392
+ let pendingToolCallIds = null;
1393
+
1394
+ for (let index = 0; index < conversationMessages.length; index += 1) {
1395
+ const message = conversationMessages[index];
1396
+ const hasToolCalls = Object.prototype.hasOwnProperty.call(
1397
+ message,
1398
+ "tool_calls",
1399
+ );
1400
+
1401
+ if (message.role === "tool") {
1402
+ const toolCallId = message.tool_call_id;
1403
+ if (phase !== "expect-tools" || !pendingToolCallIds?.delete(toolCallId)) {
1404
+ throw invalidReplRecovery(
1405
+ `REPL session tool result[${index}] has no matching pending call`,
1406
+ "CC_REPL_SESSION_TOOL_PAIR_INVALID",
1407
+ );
1408
+ }
1409
+ if (pendingToolCallIds.size === 0) {
1410
+ pendingToolCallIds = null;
1411
+ phase = "expect-assistant-after-tools";
1412
+ }
1413
+ continue;
1414
+ }
1415
+
1416
+ if (message.role === "user") {
1417
+ if (
1418
+ phase !== "expect-user" &&
1419
+ phase !== "expect-initial-conversation-message"
1420
+ ) {
1421
+ throw invalidReplRecovery(
1422
+ `REPL session user message[${index}] breaks role alternation`,
1423
+ "CC_REPL_SESSION_ROLE_ALTERNATION_INVALID",
1424
+ );
1425
+ }
1426
+ phase = "expect-assistant";
1427
+ continue;
1428
+ }
1429
+
1430
+ if (
1431
+ phase !== "expect-assistant" &&
1432
+ phase !== "expect-assistant-after-tools" &&
1433
+ phase !== "expect-initial-conversation-message"
1434
+ ) {
1435
+ throw invalidReplRecovery(
1436
+ `REPL session assistant message[${index}] breaks role alternation`,
1437
+ "CC_REPL_SESSION_ROLE_ALTERNATION_INVALID",
1438
+ );
1439
+ }
1440
+
1441
+ const toolCalls = hasToolCalls ? message.tool_calls : [];
1442
+ if (toolCalls.length === 0) {
1443
+ phase = "expect-user";
1444
+ continue;
1445
+ }
1446
+
1447
+ // IDs are unique only within this pending assistant batch. A later, fully
1448
+ // settled fallback round may legitimately reuse a deterministic ID such as
1449
+ // call_<toolName>.
1450
+ pendingToolCallIds = new Set(toolCalls.map((toolCall) => toolCall.id));
1451
+ phase = "expect-tools";
1452
+ }
1453
+
1454
+ if (phase === "expect-tools" || phase === "expect-assistant-after-tools") {
1455
+ throw invalidReplRecovery(
1456
+ "REPL session ends with an incomplete tool call/result exchange",
1457
+ "CC_REPL_SESSION_TOOL_PAIR_INVALID",
1458
+ );
1459
+ }
1460
+ return Object.freeze({
1461
+ canonicalSystemMessages,
1462
+ conversationMessages,
1463
+ replayMessages: Object.freeze([
1464
+ ...canonicalSystemMessages,
1465
+ ...conversationMessages,
1466
+ ]),
1467
+ });
1468
+ }
1469
+
1470
+ function failedReplJsonlResumeCandidate(sessionId, error, sessionSnapshot) {
1471
+ const observedCode = safeRecoveryErrorValue(
1472
+ error,
1473
+ "code",
1474
+ "CC_SESSION_HOST_SNAPSHOT_UNVERIFIED",
1475
+ );
1476
+ const resumeError = replMcpRecoveryError(
1477
+ error,
1478
+ typeof observedCode === "string" && observedCode
1479
+ ? observedCode
1480
+ : "CC_SESSION_HOST_SNAPSHOT_UNVERIFIED",
1481
+ );
1482
+ return brandedFrozenCandidate(REPL_JSONL_RESUME_CANDIDATES, {
1483
+ ok: false,
1484
+ sessionId,
1485
+ rebuiltMessages: null,
1486
+ replayMessages: null,
1487
+ canonicalSystemMessages: null,
1488
+ conversationMessages: null,
1489
+ sessionSnapshot: sessionSnapshot || null,
1490
+ mcp: failClosedReplMcpRecoveryCandidate(
1491
+ sessionId,
1492
+ resumeError,
1493
+ resumeError.code,
1494
+ "session transcript",
1495
+ ),
1496
+ error: resumeError,
1497
+ });
1498
+ }
1499
+
1500
+ function absentReplJsonlResumeCandidate(sessionId) {
1501
+ const error = new Error(`Session not found: ${sessionId}`);
1502
+ error.code = "CC_REPL_SESSION_NOT_FOUND";
1503
+ const candidate = failedReplJsonlResumeCandidate(sessionId, error, null);
1504
+ REPL_JSONL_RESUME_ABSENCE_CANDIDATES.add(candidate);
1505
+ return candidate;
1506
+ }
1507
+
1508
+ /**
1509
+ * Prepare a JSONL resume transaction from exactly one fully verified sample.
1510
+ * Messages, the public snapshot, and MCP recovery authority are derived by the
1511
+ * shared reader from that same event array. A present-but-invalid transcript is
1512
+ * an inert structured refusal and cannot be committed into the active REPL.
1513
+ */
1514
+ export function prepareReplJsonlResumeCandidate(sessionId, dependencies = {}) {
1515
+ let sessionSnapshot = null;
1516
+ try {
1517
+ validateReplResumeSessionId(sessionId);
1518
+ const readResumeState = dependencyDataFunction(
1519
+ dependencies,
1520
+ "readSessionHostResumeState",
1521
+ readSessionHostResumeState,
1522
+ );
1523
+ const formatNotice = dependencyDataFunction(
1524
+ dependencies,
1525
+ "formatMcpLedgerRecoveryNotice",
1526
+ formatMcpLedgerRecoveryNotice,
1527
+ );
1528
+ const state = synchronousReplRecoveryValue(
1529
+ readResumeState(sessionId),
1530
+ "REPL session host resume state",
1531
+ );
1532
+ if (state === null) return absentReplJsonlResumeCandidate(sessionId);
1533
+ const stateDescriptors = plainDataDescriptors(
1534
+ state,
1535
+ "REPL session host resume state",
1536
+ );
1537
+ sessionSnapshot = snapshotReplMessageValue(
1538
+ dataDescriptorValue(
1539
+ stateDescriptors,
1540
+ "snapshot",
1541
+ "REPL session host resume state",
1542
+ ),
1543
+ "REPL session host snapshot",
1544
+ );
1545
+ if (
1546
+ !isVerifiedSessionHostSnapshot(sessionSnapshot) ||
1547
+ sessionSnapshot.sessionId !== sessionId
1548
+ ) {
1549
+ const error = new Error(
1550
+ "Canonical JSONL session is not fully verified; resume was refused",
1551
+ );
1552
+ error.code = "CC_SESSION_HOST_SNAPSHOT_UNVERIFIED";
1553
+ throw error;
1554
+ }
1555
+ const rawRebuiltMessages = snapshotReplMessages(
1556
+ dataDescriptorValue(
1557
+ stateDescriptors,
1558
+ "messages",
1559
+ "REPL session host resume state",
1560
+ ),
1561
+ );
1562
+ // The injected host seam may supply plain persisted fields. Only the
1563
+ // runtime-only provenance already established by the verified store is
1564
+ // canonical; unmarked host prompts and forged wire tags are removed before
1565
+ // REPL normalization or any side effect.
1566
+ const rebuiltMessages = snapshotReplMessages(
1567
+ projectCanonicalResumeMessages(rawRebuiltMessages),
1568
+ );
1569
+ const normalizedReplay = normalizeReplReplayMessages(rebuiltMessages);
1570
+ const mcp = createReplMcpRecoveryCandidate(
1571
+ sessionId,
1572
+ dataDescriptorValue(
1573
+ stateDescriptors,
1574
+ "recovery",
1575
+ "REPL session host resume state",
1576
+ ),
1577
+ formatNotice,
1578
+ );
1579
+ if (
1580
+ mcp.recoveryError ||
1581
+ mcp.recovery.headHash !== sessionSnapshot.head.hash ||
1582
+ mcp.recovery.recoveryDigest !==
1583
+ sessionSnapshot.recoveryAuthority.recoveryDigest
1584
+ ) {
1585
+ const error = new Error(
1586
+ "Canonical JSONL messages and MCP recovery authority do not share one head",
1587
+ );
1588
+ error.code = "CC_REPL_SESSION_AUTHORITY_MISMATCH";
1589
+ throw error;
1590
+ }
1591
+ return brandedFrozenCandidate(REPL_JSONL_RESUME_CANDIDATES, {
1592
+ ok: true,
1593
+ sessionId,
1594
+ rebuiltMessages,
1595
+ replayMessages: normalizedReplay.replayMessages,
1596
+ canonicalSystemMessages: normalizedReplay.canonicalSystemMessages,
1597
+ conversationMessages: normalizedReplay.conversationMessages,
1598
+ sessionSnapshot,
1599
+ mcp,
1600
+ error: null,
1601
+ });
1602
+ } catch (error) {
1603
+ return failedReplJsonlResumeCandidate(sessionId, error, sessionSnapshot);
1604
+ }
1605
+ }
1606
+
1607
+ /**
1608
+ * Resolve startup storage only after a requested canonical JSONL session has
1609
+ * been sampled and verified. A present canonical session is authoritative;
1610
+ * feature/config reads are reserved for new sessions or the legacy DB
1611
+ * fallback when no JSONL transcript exists.
1612
+ */
1613
+ export function prepareReplStartupResume(sessionId, dependencies = {}) {
1614
+ if (sessionId !== null && sessionId !== undefined) {
1615
+ const candidate = prepareReplJsonlResumeCandidate(sessionId, dependencies);
1616
+ if (candidate.ok) {
1617
+ return Object.freeze({ useJsonl: true, candidate });
1618
+ }
1619
+ if (!REPL_JSONL_RESUME_ABSENCE_CANDIDATES.has(candidate)) {
1620
+ return Object.freeze({ useJsonl: true, candidate });
1621
+ }
1622
+ }
1623
+
1624
+ const readFeature = dependencyDataFunction(dependencies, "feature", feature);
1625
+ return Object.freeze({
1626
+ useJsonl: Boolean(readFeature("JSONL_SESSION")),
1627
+ candidate: null,
1628
+ });
1629
+ }
1630
+
1631
+ function assertReplStartupAdmission(value) {
1632
+ synchronousReplRecoveryValue(value, "REPL startup admission");
1633
+ const descriptors = plainDataDescriptors(value, "REPL startup admission");
1634
+ const useJsonl = dataDescriptorValue(
1635
+ descriptors,
1636
+ "useJsonl",
1637
+ "REPL startup admission",
1638
+ );
1639
+ const candidate = dataDescriptorValue(
1640
+ descriptors,
1641
+ "candidate",
1642
+ "REPL startup admission",
1643
+ );
1644
+ if (
1645
+ typeof useJsonl !== "boolean" ||
1646
+ !Object.isFrozen(value) ||
1647
+ (candidate !== null &&
1648
+ (!REPL_JSONL_RESUME_CANDIDATES.has(candidate) ||
1649
+ !Object.isFrozen(candidate))) ||
1650
+ (candidate !== null && useJsonl !== true)
1651
+ ) {
1652
+ throw invalidReplRecovery(
1653
+ "REPL startup admission capability is invalid",
1654
+ "CC_REPL_STARTUP_ADMISSION_INVALID",
1655
+ );
1656
+ }
1657
+ return value;
1658
+ }
1659
+
1660
+ function refuseReplStartupResume(options, candidate) {
1661
+ const observedCode = safeRecoveryErrorValue(
1662
+ candidate?.error,
1663
+ "code",
1664
+ "CC_SESSION_HOST_SNAPSHOT_UNVERIFIED",
1665
+ );
1666
+ const refusal = Object.freeze({
1667
+ started: false,
1668
+ exitCode: 1,
1669
+ code:
1670
+ typeof observedCode === "string" && observedCode
1671
+ ? observedCode
1672
+ : "CC_SESSION_HOST_SNAPSHOT_UNVERIFIED",
1673
+ sessionId: options.sessionId,
1674
+ sessionSnapshot: candidate?.sessionSnapshot || null,
1675
+ });
1676
+ logger.error(
1677
+ JSON.stringify({
1678
+ type: "session.resume.refused",
1679
+ ...refusal,
1680
+ }),
1681
+ );
1682
+ process.exitCode = 1;
1683
+ return refusal;
1684
+ }
1685
+
1686
+ function commitPreparedReplResume(prepared, commit, rollback) {
1687
+ if (!prepared || typeof commit !== "function") return false;
1688
+ try {
1689
+ commit(prepared);
1690
+ return true;
1691
+ } catch (commitError) {
1692
+ if (typeof rollback === "function") {
1693
+ try {
1694
+ rollback(prepared, commitError);
1695
+ } catch (rollbackError) {
1696
+ const error = new AggregateError(
1697
+ [commitError, rollbackError],
1698
+ "REPL session resume and rollback both failed",
1699
+ );
1700
+ error.code = "CC_REPL_SESSION_RESUME_ROLLBACK_FAILED";
1701
+ throw error;
1702
+ }
1703
+ }
1704
+ throw commitError;
1705
+ }
1706
+ }
1707
+
1708
+ /** Commit a fully prepared JSONL switch; forged/failed candidates are inert. */
1709
+ export function commitPreparedReplJsonlResume(candidate, commit, rollback) {
1710
+ if (
1711
+ !candidate ||
1712
+ !REPL_JSONL_RESUME_CANDIDATES.has(candidate) ||
1713
+ !candidate.ok
1714
+ ) {
1715
+ return false;
1716
+ }
1717
+ return commitPreparedReplResume(candidate, commit, rollback);
1718
+ }
1719
+
1720
+ /** Commit the already parsed and snapshotted legacy-DB resume state. */
1721
+ export function commitPreparedReplDbResume(preparedState, commit, rollback) {
1722
+ return commitPreparedReplResume(preparedState, commit, rollback);
475
1723
  }
476
1724
 
477
1725
  /**
478
- * Start the agentic REPL
1726
+ * Own the exact mutable state switched by the interactive `/session resume`
1727
+ * command. Both JSONL and legacy-DB paths use this controller, which also gives
1728
+ * tests a behavioral seam for failures at each real apply stage.
479
1729
  */
480
- export async function startAgentRepl(options = {}) {
481
- const trustedWorkspaceRoot = process.cwd();
482
- return runWithHostHooksV2Workspace(trustedWorkspaceRoot, () =>
483
- startAgentReplInWorkspace(options),
1730
+ export function createReplResumeStateController(bindings) {
1731
+ const snapshots = new WeakSet();
1732
+ const hostSystemMessagePrefixes = new WeakSet();
1733
+ let registeredHostSystemMessages = null;
1734
+ const replaceArray = (target, values) => {
1735
+ target.length = 0;
1736
+ for (const value of values) target.push(value);
1737
+ };
1738
+ const invalidHostPrefix = (message) =>
1739
+ invalidReplRecovery(message, "CC_REPL_HOST_SYSTEM_PREFIX_INVALID");
1740
+ const assertHostSystemMessages = (candidate) => {
1741
+ if (
1742
+ !candidate ||
1743
+ !hostSystemMessagePrefixes.has(candidate) ||
1744
+ !Array.isArray(candidate) ||
1745
+ !Object.isFrozen(candidate) ||
1746
+ candidate.length === 0 ||
1747
+ candidate.some(
1748
+ (message) =>
1749
+ !message || message.role !== "system" || !Object.isFrozen(message),
1750
+ )
1751
+ ) {
1752
+ throw invalidHostPrefix("REPL host system prefix capability is invalid");
1753
+ }
1754
+ return candidate;
1755
+ };
1756
+ const createHostSystemMessages = (sourceMessages) => {
1757
+ const snapshot = snapshotReplMessages(sourceMessages);
1758
+ const leadingSystems = [];
1759
+ for (const message of snapshot) {
1760
+ if (message.role !== "system") break;
1761
+ leadingSystems.push(message);
1762
+ }
1763
+ if (leadingSystems.length === 0) {
1764
+ throw invalidHostPrefix("REPL host system prefix is empty");
1765
+ }
1766
+ const capability = Object.freeze(leadingSystems);
1767
+ hostSystemMessagePrefixes.add(capability);
1768
+ return capability;
1769
+ };
1770
+ const registerHostSystemMessages = () => {
1771
+ if (registeredHostSystemMessages !== null) {
1772
+ throw invalidHostPrefix("REPL host system prefix is already registered");
1773
+ }
1774
+ registeredHostSystemMessages = createHostSystemMessages(bindings.messages);
1775
+ return registeredHostSystemMessages;
1776
+ };
1777
+ const refreshHostSystemMessages = () => {
1778
+ const registered = assertHostSystemMessages(registeredHostSystemMessages);
1779
+ const currentBase = snapshotReplMessages([bindings.messages[0]])[0];
1780
+ if (currentBase.role !== "system") {
1781
+ throw invalidHostPrefix("REPL current host base system is invalid");
1782
+ }
1783
+ const capability = Object.freeze([currentBase, ...registered.slice(1)]);
1784
+ hostSystemMessagePrefixes.add(capability);
1785
+ registeredHostSystemMessages = capability;
1786
+ return capability;
1787
+ };
1788
+ const materializeHostSystemMessages = (candidate) =>
1789
+ assertHostSystemMessages(candidate).map((message) => ({ ...message }));
1790
+
1791
+ const capture = () => {
1792
+ const hostSystemMessages = assertHostSystemMessages(
1793
+ registeredHostSystemMessages,
1794
+ );
1795
+ const mcpRuntime = bindings.runtimeManager.current;
1796
+ if (!mcpRuntime) {
1797
+ const error = new Error("Active MCP recovery runtime is unavailable");
1798
+ error.code = "CC_REPL_MCP_RUNTIME_UNAVAILABLE";
1799
+ throw error;
1800
+ }
1801
+ const snapshot = Object.freeze({
1802
+ sessionId: bindings.sessionId,
1803
+ messages: Object.freeze(bindings.messages.slice()),
1804
+ recovery: bindings.recovery,
1805
+ recoveryError: bindings.recoveryError,
1806
+ sanitizeRolesNextTurn: bindings.sanitizeRolesNextTurn,
1807
+ turnBindingProducer: bindings.turnBindingProducer,
1808
+ turnBindingCriticalError: bindings.turnBindingCriticalError,
1809
+ checkpointMarks: Object.freeze(bindings.checkpointMarks.slice()),
1810
+ clearedConversation: bindings.clearedConversation,
1811
+ hostSystemMessages,
1812
+ mcpRuntime,
1813
+ });
1814
+ snapshots.add(snapshot);
1815
+ return snapshot;
1816
+ };
1817
+
1818
+ const restore = (snapshot) => {
1819
+ if (!snapshot || !snapshots.has(snapshot)) {
1820
+ throw new TypeError("REPL resume snapshot is invalid");
1821
+ }
1822
+ bindings.sessionId = snapshot.sessionId;
1823
+ replaceArray(bindings.messages, snapshot.messages);
1824
+ bindings.recovery = snapshot.recovery;
1825
+ bindings.recoveryError = snapshot.recoveryError;
1826
+ bindings.sanitizeRolesNextTurn = snapshot.sanitizeRolesNextTurn;
1827
+ bindings.turnBindingProducer = snapshot.turnBindingProducer;
1828
+ bindings.turnBindingCriticalError = snapshot.turnBindingCriticalError;
1829
+ replaceArray(bindings.checkpointMarks, snapshot.checkpointMarks);
1830
+ bindings.clearedConversation = snapshot.clearedConversation;
1831
+ registeredHostSystemMessages = assertHostSystemMessages(
1832
+ snapshot.hostSystemMessages,
1833
+ );
1834
+ if (bindings.runtimeManager.current !== snapshot.mcpRuntime) {
1835
+ bindings.runtimeManager.commit(snapshot.mcpRuntime);
1836
+ }
1837
+ };
1838
+
1839
+ const apply = (preparedState) => {
1840
+ const hostSystemMessages = assertHostSystemMessages(
1841
+ preparedState.hostSystemMessages,
1842
+ );
1843
+ const activeHostSystemMessages =
1844
+ materializeHostSystemMessages(hostSystemMessages);
1845
+ const canonicalSystemMessages = snapshotReplMessages(
1846
+ preparedState.canonicalSystemMessages,
1847
+ );
1848
+ const conversationMessages = snapshotReplMessages(
1849
+ preparedState.conversationMessages,
1850
+ );
1851
+ if (
1852
+ canonicalSystemMessages.some((message) => message.role !== "system") ||
1853
+ conversationMessages.some((message) => message.role === "system")
1854
+ ) {
1855
+ throw invalidHostPrefix(
1856
+ "REPL prepared resume message partition is invalid",
1857
+ );
1858
+ }
1859
+ bindings.sessionId = preparedState.sessionId;
1860
+ replaceArray(bindings.messages, activeHostSystemMessages);
1861
+ registeredHostSystemMessages = hostSystemMessages;
1862
+ for (const message of canonicalSystemMessages) {
1863
+ bindings.messages.push(message);
1864
+ }
1865
+ bindings.applyMcpRecoveryCommit(bindings.messages, preparedState.mcpCommit);
1866
+ for (const message of conversationMessages) {
1867
+ bindings.messages.push(message);
1868
+ }
1869
+ bindings.sanitizeRolesNextTurn = preparedState.sanitizeRolesNextTurn;
1870
+ bindings.turnBindingProducer = null;
1871
+ bindings.turnBindingCriticalError = null;
1872
+ bindings.checkpointMarks.length = 0;
1873
+ bindings.clearedConversation = null;
1874
+ bindings.runtimeManager.commit(preparedState.mcpRuntime);
1875
+ bindings.logMcpRecoveryCommit(preparedState.mcpCommit);
1876
+ bindings.logger.info(preparedState.logMessage);
1877
+ };
1878
+
1879
+ return Object.freeze({
1880
+ capture,
1881
+ restore,
1882
+ apply,
1883
+ registerHostSystemMessages,
1884
+ refreshHostSystemMessages,
1885
+ });
1886
+ }
1887
+
1888
+ /**
1889
+ * Own the single recovery runtime for the active REPL session/client pair.
1890
+ * Session switches are prepared without mutating the active runtime, then
1891
+ * committed only after the transcript switch succeeds.
1892
+ */
1893
+ export function createReplMcpHostRuntimeManager(dependencies = {}) {
1894
+ const createRuntime =
1895
+ dependencies.createMcpHostRecoveryRuntime || createMcpHostRecoveryRuntime;
1896
+ const createLedgerSink =
1897
+ dependencies.createSessionMcpLedgerSink || createSessionMcpLedgerSink;
1898
+ const appendLedgerEvent =
1899
+ dependencies.appendAuthorityEvent || appendAuthorityEvent;
1900
+ const candidates = new WeakSet();
1901
+ let current = null;
1902
+
1903
+ const prepare = ({
1904
+ adhocMcp = null,
1905
+ bundleMcpClient = null,
1906
+ sessionId = null,
1907
+ persistent = false,
1908
+ recovery = null,
1909
+ recoveryError = null,
1910
+ verifiedRecovery = false,
1911
+ } = {}) => {
1912
+ const bundle = adhocMcp?.mcpClient
1913
+ ? adhocMcp
1914
+ : bundleMcpClient
1915
+ ? { mcpClient: bundleMcpClient }
1916
+ : null;
1917
+ const rawClient = bundle?.mcpClient || null;
1918
+ const persistLedger = Boolean(persistent && sessionId);
1919
+
1920
+ if (
1921
+ !verifiedRecovery &&
1922
+ current &&
1923
+ current.rawClient === rawClient &&
1924
+ current.sessionId === sessionId &&
1925
+ current.persistent === persistLedger &&
1926
+ current.recovery === recovery &&
1927
+ current.recoveryError === recoveryError
1928
+ ) {
1929
+ return current;
1930
+ }
1931
+
1932
+ const sink = persistLedger
1933
+ ? createLedgerSink(sessionId, { appendEvent: appendLedgerEvent })
1934
+ : null;
1935
+ const sharedController =
1936
+ !verifiedRecovery && current?.sessionId === sessionId
1937
+ ? current.runtime.controller
1938
+ : null;
1939
+ const runtime = createRuntime({
1940
+ bundle,
1941
+ rawClient,
1942
+ sessionId,
1943
+ sink,
1944
+ recovery,
1945
+ recoveryError,
1946
+ ...(sharedController ? { controller: sharedController } : {}),
1947
+ });
1948
+ const candidate = Object.freeze({
1949
+ runtime,
1950
+ rawClient,
1951
+ hostMcp: bundle
1952
+ ? Object.freeze({
1953
+ ...bundle,
1954
+ mcpClient: runtime.client,
1955
+ })
1956
+ : null,
1957
+ sessionId,
1958
+ persistent: persistLedger,
1959
+ recovery,
1960
+ recoveryError,
1961
+ verifiedRecovery: Boolean(verifiedRecovery),
1962
+ });
1963
+ candidates.add(candidate);
1964
+ return candidate;
1965
+ };
1966
+
1967
+ const commit = (candidate) => {
1968
+ if (!candidate || !candidates.has(candidate)) {
1969
+ throw new TypeError("MCP host runtime candidate is invalid");
1970
+ }
1971
+ current = candidate;
1972
+ return current;
1973
+ };
1974
+
1975
+ return Object.freeze({
1976
+ prepare,
1977
+ commit,
1978
+ activate(options) {
1979
+ return commit(prepare(options));
1980
+ },
1981
+ get current() {
1982
+ return current;
1983
+ },
1984
+ });
1985
+ }
1986
+
1987
+ /**
1988
+ * Execute the startup boundary with explicit host-owned dependencies. Exported
1989
+ * for behavior tests; callers cannot obtain the private workspace starter, and
1990
+ * the public startAgentRepl() below always supplies the real fixed bindings.
1991
+ */
1992
+ export async function runReplStartupBoundary(options, startupDependencies) {
1993
+ const prepareStartup = dependencyDataFunction(
1994
+ startupDependencies,
1995
+ "prepareReplStartupResume",
1996
+ null,
1997
+ );
1998
+ if (typeof prepareStartup !== "function") {
1999
+ throw invalidReplRecovery(
2000
+ "REPL startup admission dependency is missing",
2001
+ "CC_REPL_STARTUP_DEPENDENCY_INVALID",
2002
+ );
2003
+ }
2004
+ const startupAdmission = assertReplStartupAdmission(
2005
+ synchronousReplRecoveryValue(
2006
+ prepareStartup(options.sessionId),
2007
+ "REPL startup admission",
2008
+ ),
2009
+ );
2010
+ const startupCandidate = startupAdmission.candidate;
2011
+ if (startupCandidate && !startupCandidate.ok) {
2012
+ const refuseStartup = dependencyDataFunction(
2013
+ startupDependencies,
2014
+ "refuseReplStartupResume",
2015
+ null,
2016
+ );
2017
+ if (typeof refuseStartup !== "function") {
2018
+ throw invalidReplRecovery(
2019
+ "REPL startup refusal dependency is missing",
2020
+ "CC_REPL_STARTUP_DEPENDENCY_INVALID",
2021
+ );
2022
+ }
2023
+ return refuseStartup(options, startupCandidate);
2024
+ }
2025
+
2026
+ // Only a frozen, branded admission capability may cross into workspace,
2027
+ // pipe, config, plugin, hook, MCP, model, or tool initialization.
2028
+ const readCwd = dependencyDataFunction(startupDependencies, "cwd", null);
2029
+ const enterWorkspace = dependencyDataFunction(
2030
+ startupDependencies,
2031
+ "runWithHostHooksV2Workspace",
2032
+ null,
2033
+ );
2034
+ const startWorkspace = dependencyDataFunction(
2035
+ startupDependencies,
2036
+ "startAgentReplInWorkspace",
2037
+ null,
2038
+ );
2039
+ if (
2040
+ typeof readCwd !== "function" ||
2041
+ typeof enterWorkspace !== "function" ||
2042
+ typeof startWorkspace !== "function"
2043
+ ) {
2044
+ throw invalidReplRecovery(
2045
+ "REPL host startup dependency is missing",
2046
+ "CC_REPL_STARTUP_DEPENDENCY_INVALID",
2047
+ );
2048
+ }
2049
+ const trustedWorkspaceRoot = readCwd();
2050
+ return enterWorkspace(trustedWorkspaceRoot, () =>
2051
+ startWorkspace(options, startupAdmission),
484
2052
  );
485
2053
  }
486
2054
 
487
- async function startAgentReplInWorkspace(options = {}) {
2055
+ /** Start the agentic REPL with non-overridable production bindings. */
2056
+ export async function startAgentRepl(options = {}) {
2057
+ return runReplStartupBoundary(options, {
2058
+ prepareReplStartupResume,
2059
+ refuseReplStartupResume,
2060
+ cwd: () => process.cwd(),
2061
+ runWithHostHooksV2Workspace,
2062
+ startAgentReplInWorkspace,
2063
+ });
2064
+ }
2065
+
2066
+ async function startAgentReplInWorkspace(options = {}, startupAdmission) {
488
2067
  // EPIPE guard: if the REPL's stdout is piped and the consumer closes (e.g.
489
2068
  // `cc agent | head`), the async stream `error` would otherwise crash the
490
2069
  // process. Route a broken pipe into the REPL's own graceful shutdown (the
@@ -506,6 +2085,7 @@ async function startAgentReplInWorkspace(options = {}) {
506
2085
  }
507
2086
  process.exit(0);
508
2087
  });
2088
+ const { useJsonl, candidate: _startupJsonlResume } = startupAdmission;
509
2089
  let model = options.model || "qwen2.5:7b";
510
2090
  let provider = options.provider || "ollama";
511
2091
  // Extended thinking (Anthropic; opt-in via --think/--ultrathink). Carried from
@@ -515,12 +2095,15 @@ async function startAgentReplInWorkspace(options = {}) {
515
2095
  // (--thinking-budget) is the companion legacy-model budget_tokens override.
516
2096
  let thinking = options.thinking || null;
517
2097
  const thinkingBudget = options.thinkingBudget || null;
518
- // `/btw` one-shot asides: queued notes that ride ONLY the next turn (sent to
519
- // the model, then stripped so they never persist or carry forward).
520
- // `_btwRestore` holds the user message + its pre-aside content so we can undo
521
- // the injection after the turn (or as a backstop at the next submit on throw).
2098
+ // `/note-next` one-shot guidance rides ONLY the next main turn, then is
2099
+ // stripped so it never persists or carries forward. `/btw` itself is an
2100
+ // immediate independent side question handled below.
522
2101
  let pendingBtw = [];
523
2102
  let _btwRestore = null;
2103
+ // Provider-neutral, tool-free Advisor/Critic. Initialized lazily after the
2104
+ // durable session and resumed transcript are available; null means the
2105
+ // optional surface could not be initialized. Default is off via config.
2106
+ let _advisorRuntime = null;
524
2107
  // Current permission mode (strict|trusted|autopilot|auto), mirrored here so
525
2108
  // Shift+Tab can cycle it and `/permissions <tier>` can set it. Kept in sync
526
2109
  // with _approvalGate.setSessionPolicy below. "auto" rides the trusted gate
@@ -588,6 +2171,13 @@ async function startAgentReplInWorkspace(options = {}) {
588
2171
  // Snapshot the work tree before each mutating tool (git engine) so the user
589
2172
  // can `cc checkpoint restore` to just before any tool call.
590
2173
  const autoCheckpoint = options.autoCheckpoint === true;
2174
+ const managedCheckpoint = options.managedCheckpoint === true;
2175
+ const managedCheckpointStateDir = options.managedCheckpointStateDir || null;
2176
+ const managedCheckpointExclusions = Array.isArray(
2177
+ options.managedCheckpointExclusions,
2178
+ )
2179
+ ? options.managedCheckpointExclusions
2180
+ : [];
591
2181
 
592
2182
  // --fallback-model: walk an ordered backup-model chain when a turn's LLM
593
2183
  // call fails (transient error or model-not-found). Built once; passed into
@@ -616,7 +2206,10 @@ async function startAgentReplInWorkspace(options = {}) {
616
2206
  // Bootstrap runtime (best-effort, DB not required)
617
2207
  let db = null;
618
2208
  let contextEngine = null;
619
- let sessionId = null;
2209
+ let sessionId = _startupJsonlResume?.sessionId || null;
2210
+ const _replCompactPersistence = createReplCompactPersistence(
2211
+ _startupJsonlResume?.ok ? _startupJsonlResume.rebuiltMessages : [],
2212
+ );
620
2213
 
621
2214
  try {
622
2215
  const ctx = await bootstrap({ verbose: false });
@@ -627,7 +2220,30 @@ async function startAgentReplInWorkspace(options = {}) {
627
2220
 
628
2221
  // Initialize prompt compressor (adaptive to model's context window)
629
2222
  if (feature("PROMPT_COMPRESSOR")) {
630
- _compressor = new PromptCompressor({ model, provider });
2223
+ _compressor = new PromptCompressor({
2224
+ model,
2225
+ provider,
2226
+ llmQuery: async (prompt) => {
2227
+ const response = await chatWithTools(
2228
+ [{ role: "user", content: prompt }],
2229
+ {
2230
+ provider,
2231
+ model,
2232
+ baseUrl,
2233
+ apiKey,
2234
+ enabledToolNames: [],
2235
+ extraToolDefinitions: [],
2236
+ maxOutputTokens: 2048,
2237
+ },
2238
+ );
2239
+ return {
2240
+ summary: response?.message?.content || "",
2241
+ usage: response?.usage || null,
2242
+ provider,
2243
+ model,
2244
+ };
2245
+ },
2246
+ });
631
2247
  }
632
2248
  const _autoPinOpt = resolveAutoPinOption({ config: _autoPinCfgValue });
633
2249
  // Compaction options shared by /compact + auto-compact. Adds the
@@ -893,16 +2509,18 @@ async function startAgentReplInWorkspace(options = {}) {
893
2509
  // PostToolUse). The interactive _permissionConfirm above doubles as the
894
2510
  // confirmer for a hook `ask` decision.
895
2511
  try {
896
- const { loadHooks, projectHookTrustNotice } =
2512
+ const { loadHooks, projectHookTrustNotice, attachAuthorityErrors } =
897
2513
  await import("../lib/settings-hooks.cjs");
898
2514
  const loaded = loadHooks({ cwd: process.cwd() });
899
2515
  // Fold in installed plugins' hooks/hooks.json (Phase 3.3c).
900
2516
  const { mergePluginHooks } = await import("../lib/plugin-runtime/hooks.js");
901
- const effectiveHooks = mergePluginHooks(loaded.hooks, {
902
- cwd: process.cwd(),
903
- });
2517
+ const effectiveHooks = mergePluginHooks(
2518
+ attachAuthorityErrors(loaded.hooks, loaded.authorityErrors),
2519
+ { cwd: process.cwd() },
2520
+ );
904
2521
  _settingsHooks =
905
- effectiveHooks && Object.keys(effectiveHooks).length > 0
2522
+ Object.keys(effectiveHooks).length > 0 ||
2523
+ effectiveHooks._authorityErrors.length > 0
906
2524
  ? effectiveHooks
907
2525
  : null;
908
2526
  // First-run trust notice for an untrusted/cloned repo's shell-running
@@ -914,7 +2532,16 @@ async function startAgentReplInWorkspace(options = {}) {
914
2532
  /* trust notice is best-effort */
915
2533
  }
916
2534
  } catch (_err) {
917
- _settingsHooks = null;
2535
+ _settingsHooks = {};
2536
+ Object.defineProperty(_settingsHooks, "_authorityErrors", {
2537
+ value: Object.freeze([
2538
+ Object.freeze({
2539
+ sourceFile: null,
2540
+ code: _err?.code || "CC_HOOK_AUTHORITY_LOAD_FAILED",
2541
+ }),
2542
+ ]),
2543
+ enumerable: false,
2544
+ });
918
2545
  }
919
2546
 
920
2547
  // Start installed plugins' background monitors (Phase 3.3i). Trust-gated
@@ -972,19 +2599,10 @@ async function startAgentReplInWorkspace(options = {}) {
972
2599
  _pluginSettingsRestore = null;
973
2600
  }
974
2601
 
975
- // Resume existing session or create new one
976
- const useJsonl = feature("JSONL_SESSION");
977
-
978
- if (useJsonl && options.sessionId) {
979
- // JSONL resume: check if session file exists
980
- try {
981
- if (sessionExists(options.sessionId)) {
982
- sessionId = options.sessionId;
983
- }
984
- } catch (_err) {
985
- // Non-critical
986
- }
987
- } else if (db && options.sessionId) {
2602
+ // Resume existing session or create new one. JSONL resumes were admitted
2603
+ // before bootstrap/config/plugin/hook initialization and use that exact
2604
+ // verified sample below; do not re-read the transcript here.
2605
+ if (!useJsonl && db && options.sessionId) {
988
2606
  try {
989
2607
  const existing = getSession(db, options.sessionId);
990
2608
  if (existing && existing.messages) {
@@ -1144,6 +2762,56 @@ async function startAgentReplInWorkspace(options = {}) {
1144
2762
  let _replBaseSystem = _buildReplBaseSystem();
1145
2763
  let _activeOutputStyle = null; // { name, body }
1146
2764
  const messages = [{ role: "system", content: _replBaseSystem }];
2765
+ let _mcpLedgerRecovery = null;
2766
+ let _mcpLedgerRecoveryError = null;
2767
+ const _prepareJsonlResumeCandidate = (targetSessionId) =>
2768
+ prepareReplJsonlResumeCandidate(targetSessionId);
2769
+ const _prepareMcpRecoveryCommit = (candidate) => {
2770
+ const authorized =
2771
+ candidate && REPL_MCP_RECOVERY_CANDIDATES.has(candidate)
2772
+ ? candidate
2773
+ : failClosedReplMcpRecoveryCandidate(
2774
+ sessionId,
2775
+ invalidReplRecovery(
2776
+ "MCP recovery candidate capability is invalid",
2777
+ "CC_REPL_MCP_RECOVERY_CANDIDATE_INVALID",
2778
+ ),
2779
+ "CC_REPL_MCP_RECOVERY_CANDIDATE_INVALID",
2780
+ );
2781
+ const recovery = authorized.recovery;
2782
+ const recoveryError = authorized.recoveryError;
2783
+ const noticeMessage = authorized.notice
2784
+ ? Object.freeze({ role: "system", content: authorized.notice })
2785
+ : null;
2786
+ const warning = recoveryError
2787
+ ? "⚠ MCP ledger recovery failed closed; inspect the transcript before using MCP tools."
2788
+ : authorized.notice
2789
+ ? `⚠ ${recovery.unsettled.length} interrupted MCP call(s), ${recovery.incidents.length} ledger incident(s), and ${recovery.replayDenied.length} exact replay deny/denies require inspection before replay.`
2790
+ : null;
2791
+ return Object.freeze({
2792
+ recovery,
2793
+ recoveryError,
2794
+ noticeMessage,
2795
+ warning,
2796
+ });
2797
+ };
2798
+ const _applyMcpRecoveryCommit = (targetMessages, preparedCommit) => {
2799
+ _mcpLedgerRecovery = preparedCommit.recovery;
2800
+ _mcpLedgerRecoveryError = preparedCommit.recoveryError;
2801
+ if (preparedCommit.noticeMessage) {
2802
+ targetMessages.push(preparedCommit.noticeMessage);
2803
+ }
2804
+ return Boolean(preparedCommit.noticeMessage);
2805
+ };
2806
+ const _logMcpRecoveryCommit = (preparedCommit) => {
2807
+ if (preparedCommit.warning) logger.warn(preparedCommit.warning);
2808
+ };
2809
+ const _commitMcpRecoveryCandidate = (targetMessages, candidate) => {
2810
+ const preparedCommit = _prepareMcpRecoveryCommit(candidate);
2811
+ const addedNotice = _applyMcpRecoveryCommit(targetMessages, preparedCommit);
2812
+ _logMcpRecoveryCommit(preparedCommit);
2813
+ return addedNotice;
2814
+ };
1147
2815
  // Resume-degenerate role sanitation (Claude Code 2.1.187 parity): a one-shot
1148
2816
  // flag armed when a resumed transcript ends with a bare `user` turn (the prior
1149
2817
  // run produced no assistant response). Consumed by the first model call so the
@@ -1420,6 +3088,89 @@ async function startAgentReplInWorkspace(options = {}) {
1420
3088
  }
1421
3089
  _persistReplContextSources();
1422
3090
 
3091
+ const _mcpHostRuntimeManager = createReplMcpHostRuntimeManager();
3092
+ const _mcpRuntimeInputs = ({
3093
+ targetSessionId = sessionId,
3094
+ recovery = _mcpLedgerRecovery,
3095
+ recoveryError = _mcpLedgerRecoveryError,
3096
+ } = {}) => ({
3097
+ adhocMcp: _adhocMcp,
3098
+ bundleMcpClient: _bundleMcpClient,
3099
+ sessionId: targetSessionId,
3100
+ persistent: useJsonl && Boolean(targetSessionId),
3101
+ recovery,
3102
+ recoveryError,
3103
+ });
3104
+ const _activateMcpHostRuntime = () =>
3105
+ _mcpHostRuntimeManager.activate(_mcpRuntimeInputs());
3106
+ const _prepareMcpHostRuntime = (targetSessionId, mcpCandidate = {}) =>
3107
+ _mcpHostRuntimeManager.prepare({
3108
+ ..._mcpRuntimeInputs({
3109
+ targetSessionId,
3110
+ recovery: mcpCandidate.recovery ?? null,
3111
+ recoveryError: mcpCandidate.recoveryError ?? null,
3112
+ }),
3113
+ verifiedRecovery: true,
3114
+ });
3115
+ const _getReplHostMcp = () => _activateMcpHostRuntime().hostMcp;
3116
+ const _resumeStateController = createReplResumeStateController({
3117
+ get sessionId() {
3118
+ return sessionId;
3119
+ },
3120
+ set sessionId(value) {
3121
+ sessionId = value;
3122
+ },
3123
+ messages,
3124
+ get recovery() {
3125
+ return _mcpLedgerRecovery;
3126
+ },
3127
+ set recovery(value) {
3128
+ _mcpLedgerRecovery = value;
3129
+ },
3130
+ get recoveryError() {
3131
+ return _mcpLedgerRecoveryError;
3132
+ },
3133
+ set recoveryError(value) {
3134
+ _mcpLedgerRecoveryError = value;
3135
+ },
3136
+ get sanitizeRolesNextTurn() {
3137
+ return _sanitizeRolesNextTurn;
3138
+ },
3139
+ set sanitizeRolesNextTurn(value) {
3140
+ _sanitizeRolesNextTurn = value;
3141
+ },
3142
+ get turnBindingProducer() {
3143
+ return _turnBindingProducer;
3144
+ },
3145
+ set turnBindingProducer(value) {
3146
+ _turnBindingProducer = value;
3147
+ },
3148
+ get turnBindingCriticalError() {
3149
+ return _turnBindingCriticalError;
3150
+ },
3151
+ set turnBindingCriticalError(value) {
3152
+ _turnBindingCriticalError = value;
3153
+ },
3154
+ checkpointMarks: _checkpointMarks,
3155
+ get clearedConversation() {
3156
+ return _clearedConversation;
3157
+ },
3158
+ set clearedConversation(value) {
3159
+ _clearedConversation = value;
3160
+ },
3161
+ runtimeManager: _mcpHostRuntimeManager,
3162
+ applyMcpRecoveryCommit: _applyMcpRecoveryCommit,
3163
+ logMcpRecoveryCommit: _logMcpRecoveryCommit,
3164
+ logger,
3165
+ });
3166
+ const _captureResumeState = _resumeStateController.capture;
3167
+ const _restoreResumeState = _resumeStateController.restore;
3168
+ const _applyPreparedResumeState = _resumeStateController.apply;
3169
+ const _registerHostSystemMessages =
3170
+ _resumeStateController.registerHostSystemMessages;
3171
+ const _refreshHostSystemMessages =
3172
+ _resumeStateController.refreshHostSystemMessages;
3173
+
1423
3174
  // Seed connected MCP servers with the startup workspace roots when the session
1424
3175
  // began with extra `--add-dir` roots — otherwise `roots/list` would only ever
1425
3176
  // return the cwd until the first mid-session /add-dir. No-op (byte-unchanged)
@@ -1500,35 +3251,37 @@ async function startAgentReplInWorkspace(options = {}) {
1500
3251
  }
1501
3252
  }
1502
3253
 
3254
+ // Seal the complete host-owned leading system prefix exactly once, after
3255
+ // startup hooks, bundle context, and optional memory injection, but before
3256
+ // any canonical/legacy replay is appended. Later live switches refresh only
3257
+ // entry 0 from the active base prompt; they never re-scan old session systems.
3258
+ _registerHostSystemMessages();
3259
+
3260
+ let _startupResumeCommitted = false;
1503
3261
  // Load resumed session messages
1504
3262
  if (options.sessionId && sessionId) {
1505
3263
  try {
1506
3264
  if (useJsonl) {
1507
- // Interactive tamper warning: a broken hash chain means the transcript
1508
- // was edited outside the store — resume proceeds (the user is present)
1509
- // but the restored context is flagged untrusted.
1510
- try {
1511
- const { verifySession } =
1512
- await import("../harness/jsonl-session-store.js");
1513
- const trust = verifySession(sessionId);
1514
- if (trust.status === "tampered") {
1515
- logger.warn(
1516
- `⚠ Transcript integrity check FAILED (${trust.reason}) — restored context is untrusted. Run 'cc session verify ${sessionId}'.`,
1517
- );
1518
- }
1519
- } catch (_err) {
1520
- // verification is best-effort in the interactive path
3265
+ const prepared = _startupJsonlResume;
3266
+ if (!prepared?.ok) {
3267
+ const error = new Error(
3268
+ "Startup JSONL resume lost its verified admission capability",
3269
+ );
3270
+ error.code = "CC_REPL_SESSION_RESUME_NOT_PREPARED";
3271
+ throw error;
1521
3272
  }
1522
- const rebuilt = rebuildMessages(sessionId);
1523
- if (rebuilt.length > 0) {
1524
- messages.push(...rebuilt.filter((m) => m.role !== "system"));
3273
+ messages.push(...prepared.canonicalSystemMessages);
3274
+ _commitMcpRecoveryCandidate(messages, prepared.mcp);
3275
+ if (prepared.replayMessages.length > 0) {
3276
+ messages.push(...prepared.conversationMessages);
1525
3277
  // Arm the resume role-merge if the prior run left a dangling user turn.
1526
3278
  _sanitizeRolesNextTurn =
1527
- messages[messages.length - 1]?.role === "user";
3279
+ prepared.conversationMessages.at(-1)?.role === "user";
1528
3280
  logger.info(
1529
- `Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
3281
+ `Resumed JSONL session ${sessionId} (${prepared.rebuiltMessages.length} messages)`,
1530
3282
  );
1531
3283
  }
3284
+ _startupResumeCommitted = true;
1532
3285
  } else if (db) {
1533
3286
  const existing = getSession(db, sessionId);
1534
3287
  if (existing && existing.messages) {
@@ -1542,15 +3295,26 @@ async function startAgentReplInWorkspace(options = {}) {
1542
3295
  logger.info(
1543
3296
  `Resumed session ${sessionId} (${parsed.length} messages)`,
1544
3297
  );
3298
+ _startupResumeCommitted = true;
1545
3299
  }
1546
3300
  }
1547
- } catch (_err) {
1548
- // Non-critical
3301
+ } catch (error) {
3302
+ if (useJsonl) {
3303
+ _commitMcpRecoveryCandidate(
3304
+ messages,
3305
+ failClosedReplMcpRecoveryCandidate(
3306
+ sessionId,
3307
+ error,
3308
+ "CC_REPL_SESSION_REBUILD_FAILED",
3309
+ "session transcript",
3310
+ ),
3311
+ );
3312
+ }
1549
3313
  }
1550
3314
  // settings.json SessionResume hooks: a persisted transcript was just
1551
3315
  // replayed into this interactive session (distinct from SessionStart).
1552
3316
  // Observe-only, best-effort — never blocks entering the REPL.
1553
- if (_settingsHooks && messages.some((m) => m.role !== "system")) {
3317
+ if (_settingsHooks && _startupResumeCommitted) {
1554
3318
  try {
1555
3319
  const { runObserveHooks } =
1556
3320
  await import("../lib/settings-hook-events.js");
@@ -1582,6 +3346,50 @@ async function startAgentReplInWorkspace(options = {}) {
1582
3346
  }
1583
3347
  }
1584
3348
 
3349
+ // Create the active session-scoped runtime only after startup recovery has
3350
+ // been loaded. Subsequent turns reuse this controller and ledger so a
3351
+ // settlement-failure latch cannot be cleared by starting another turn.
3352
+ _activateMcpHostRuntime();
3353
+
3354
+ // Advisor is independent from the main agent loop: it gets a redacted
3355
+ // transcript snapshot and ZERO tools. Compact call/outcome metadata shares
3356
+ // the session hash chain, while advice text itself is never persisted.
3357
+ try {
3358
+ const { createConfiguredAdvisorRuntime } =
3359
+ await import("../lib/advisor-runtime.js");
3360
+ const advisorOverrides = {};
3361
+ if (options.advisorEnabled !== undefined) {
3362
+ advisorOverrides.enabled = options.advisorEnabled === true;
3363
+ }
3364
+ _advisorRuntime = await createConfiguredAdvisorRuntime({
3365
+ cwd: process.cwd(),
3366
+ settingsFile: options.settingsFile || null,
3367
+ managedSettingsFile: options.managedSettingsFile || null,
3368
+ mainProvider: provider,
3369
+ mainModel: model,
3370
+ baseUrl,
3371
+ apiKey,
3372
+ overrides: advisorOverrides,
3373
+ onEvent: (event) => {
3374
+ if (!useJsonl || !sessionId) return;
3375
+ try {
3376
+ appendEvent(sessionId, event.type, event.data);
3377
+ } catch {
3378
+ // Advisor observability is best-effort and cannot fail a main turn.
3379
+ }
3380
+ },
3381
+ });
3382
+ // A resumed transcript may contain old tool results. Mark them observed so
3383
+ // the first new turn cannot trigger an Advisor call for historical work.
3384
+ _advisorRuntime.primeMessages(messages);
3385
+ } catch (error) {
3386
+ if (error?.code === "CC_MANAGED_SETTINGS_INVALID") throw error;
3387
+ _advisorRuntime = null;
3388
+ if (options.advisorEnabled === true) {
3389
+ logger.warn(`Advisor unavailable: ${error.message}`);
3390
+ }
3391
+ }
3392
+
1585
3393
  // Vim mode (Claude-Code `/vim` parity): opt-in modal line editing. `_vim`
1586
3394
  // holds the NORMAL-mode engine state while normal mode is active (readline's
1587
3395
  // own key handling is suspended then); it is null in INSERT mode (readline
@@ -1751,6 +3559,7 @@ async function startAgentReplInWorkspace(options = {}) {
1751
3559
  : [
1752
3560
  "/add-dir",
1753
3561
  "/agents",
3562
+ "/advisor",
1754
3563
  "/auto",
1755
3564
  "/btw",
1756
3565
  "/cd",
@@ -1776,6 +3585,7 @@ async function startAgentReplInWorkspace(options = {}) {
1776
3585
  "/memory",
1777
3586
  "/microcompact",
1778
3587
  "/model",
3588
+ "/note-next",
1779
3589
  "/output-style",
1780
3590
  "/permissions",
1781
3591
  "/plan",
@@ -1821,12 +3631,13 @@ async function startAgentReplInWorkspace(options = {}) {
1821
3631
  }
1822
3632
  },
1823
3633
  getIdeOpenFiles: async () => {
1824
- const exec = _adhocMcp?.externalToolExecutors?.mcp__ide__getOpenEditors;
1825
- if (!exec || exec.kind !== "mcp" || !_adhocMcp?.mcpClient?.callTool) {
3634
+ const hostMcp = _getReplHostMcp();
3635
+ const exec = hostMcp?.externalToolExecutors?.mcp__ide__getOpenEditors;
3636
+ if (!exec || exec.kind !== "mcp" || !hostMcp?.mcpClient?.callTool) {
1826
3637
  return [];
1827
3638
  }
1828
3639
  const { parseToolResultJson } = await import("../lib/ide-context.js");
1829
- const res = await _adhocMcp.mcpClient.callTool(
3640
+ const res = await hostMcp.mcpClient.callTool(
1830
3641
  exec.serverName,
1831
3642
  exec.toolName,
1832
3643
  {},
@@ -2231,9 +4042,61 @@ async function startAgentReplInWorkspace(options = {}) {
2231
4042
 
2232
4043
  // Steering (Claude-Code parity): typing while a turn is running QUEUES the
2233
4044
  // line instead of racing a second concurrent turn; the queue drains FIFO
2234
- // when the current turn finishes.
4045
+ // when the current turn finishes. `/btw` is the deliberate exception: its
4046
+ // tool-free snapshot call may run concurrently without mutating main state.
2235
4047
  let _processingLine = false;
2236
4048
  const _pendingLines = [];
4049
+
4050
+ const runBtwSideQuestion = async (btw, { concurrent = false } = {}) => {
4051
+ if (!btw || btw.error) {
4052
+ const message = btw?.error || "usage: /btw [--fork] <question>";
4053
+ if (concurrent) process.stderr.write(`\n${message}\n`);
4054
+ else logger.info(message);
4055
+ return { ok: false, error: message };
4056
+ }
4057
+ try {
4058
+ const chatFn = createChatFn({ provider, model, baseUrl, apiKey });
4059
+ const { answer } = await runBtwQuestion({
4060
+ messages,
4061
+ question: btw.text,
4062
+ chatFn,
4063
+ model,
4064
+ });
4065
+ let forkedId = null;
4066
+ let forkNotice = "";
4067
+ if (btw.fork) {
4068
+ if (useJsonl && sessionId && sessionExists(sessionId)) {
4069
+ forkedId = forkSession(sessionId, {
4070
+ requestId: `btw-${randomUUID()}`,
4071
+ });
4072
+ if (forkedId) {
4073
+ appendUserMessage(forkedId, btw.text);
4074
+ appendAssistantMessage(forkedId, answer);
4075
+ forkNotice = `\nForked side thread: ${forkedId}`;
4076
+ }
4077
+ }
4078
+ if (!forkedId) {
4079
+ forkNotice =
4080
+ "\nSide answer was not persisted: the parent has no durable JSONL session.";
4081
+ }
4082
+ }
4083
+ const block =
4084
+ `┌─ btw${btw.fork ? " --fork" : ""}\n` +
4085
+ `${answer || "(empty response)"}\n` +
4086
+ `└─ ephemeral · no tools · parent history unchanged${forkNotice}`;
4087
+ // A concurrent answer must not corrupt the main streamed stdout payload.
4088
+ // stderr also keeps headless/JSON consumers isolated from the overlay.
4089
+ if (concurrent) process.stderr.write(`\n${block}\n`);
4090
+ else logger.log(`\n${block}\n`);
4091
+ return { ok: true, answer, forkedId };
4092
+ } catch (error) {
4093
+ const message = `/btw failed: ${error.message}`;
4094
+ if (concurrent) process.stderr.write(`\n${message}\n`);
4095
+ else logger.error(message);
4096
+ return { ok: false, error: message };
4097
+ }
4098
+ };
4099
+
2237
4100
  // Multiline input (Claude-Code parity): a physical line ending in a
2238
4101
  // continuation backslash keeps the prompt open; `_mlBuffer` accumulates the
2239
4102
  // pieces and the whole block submits when a line does not continue.
@@ -2368,7 +4231,13 @@ async function startAgentReplInWorkspace(options = {}) {
2368
4231
  ` ${chalk.cyan("/effort")} Reasoning effort (/effort low|medium|high|xhigh; Anthropic)`,
2369
4232
  );
2370
4233
  logger.log(
2371
- ` ${chalk.cyan("/btw")} Queue a one-off aside for your next message (not saved to history)`,
4234
+ ` ${chalk.cyan("/btw")} Ask an immediate tool-free side question (/btw [--fork] <question>)`,
4235
+ );
4236
+ logger.log(
4237
+ ` ${chalk.cyan("/advisor")} Independent tool-free critic (/advisor on|off|once [focus]|status)`,
4238
+ );
4239
+ logger.log(
4240
+ ` ${chalk.cyan("/note-next")} Apply ephemeral guidance to the next main turn`,
2372
4241
  );
2373
4242
  logger.log(` ${chalk.cyan("/clear")} Clear conversation`);
2374
4243
  logger.log(
@@ -2907,22 +4776,52 @@ async function startAgentReplInWorkspace(options = {}) {
2907
4776
  }
2908
4777
  }
2909
4778
 
2910
- // `/btw <note>` — queue a one-shot aside (Claude-Code parity). It rides the
2911
- // NEXT message to the model, then is stripped so it never bloats history.
4779
+ // `/btw` is an immediate, tool-free side question over a snapshot of the
4780
+ // current context. It never mutates or persists into the parent history.
2912
4781
  {
2913
4782
  const btw = parseBtwCommand(trimmed);
2914
4783
  if (btw) {
2915
- if (btw.error) {
2916
- logger.info(btw.error);
2917
- prompt();
2918
- return;
4784
+ await runBtwSideQuestion(btw);
4785
+ prompt();
4786
+ return;
4787
+ }
4788
+ }
4789
+
4790
+ // `/advisor`: session-local enable/disable, one forced review, or status.
4791
+ // One-shot advice is queued as an ephemeral prepareCall suffix, requiring
4792
+ // the main agent to validate it locally on its next model/tool turn.
4793
+ {
4794
+ const { parseAdvisorCommand, executeAdvisorCommand } =
4795
+ await import("./advisor-command.js");
4796
+ const advisorCommand = parseAdvisorCommand(trimmed);
4797
+ if (advisorCommand) {
4798
+ const result = await executeAdvisorCommand(advisorCommand, {
4799
+ runtime: _advisorRuntime,
4800
+ messages,
4801
+ signal: _turnAbort?.signal || null,
4802
+ });
4803
+ if (result?.output) logger.log(result.output);
4804
+ if (result?.guidance) {
4805
+ _advisorRuntime?.queueGuidance(result.guidance);
4806
+ }
4807
+ prompt();
4808
+ return;
4809
+ }
4810
+ }
4811
+
4812
+ // Preserve the previous next-turn guidance behavior under an honest name.
4813
+ {
4814
+ const note = parseNoteNextCommand(trimmed);
4815
+ if (note) {
4816
+ if (note.error) logger.info(note.error);
4817
+ else {
4818
+ pendingBtw.push(note.text);
4819
+ logger.info(
4820
+ chalk.gray(
4821
+ `Next-turn note queued (${pendingBtw.length}); it will not be saved to history.`,
4822
+ ),
4823
+ );
2919
4824
  }
2920
- pendingBtw.push(btw.text);
2921
- logger.info(
2922
- chalk.gray(
2923
- `Aside noted (${pendingBtw.length}) — rides your next message only, not saved to history.`,
2924
- ),
2925
- );
2926
4825
  prompt();
2927
4826
  return;
2928
4827
  }
@@ -3665,6 +5564,11 @@ async function startAgentReplInWorkspace(options = {}) {
3665
5564
  logger.info(
3666
5565
  `Compacted: ${stats.originalMessages} → ${stats.compressedMessages} messages, saved ${stats.saved} tokens (${stats.strategy})`,
3667
5566
  );
5567
+ if (stats.degraded === true) {
5568
+ logger.warn(
5569
+ `Semantic compaction degraded to ${stats.summaryMode}: ${stats.degradedReason}`,
5570
+ );
5571
+ }
3668
5572
  } else if (contextEngine && messages.length > 5) {
3669
5573
  const compacted = contextEngine.smartCompact(messages);
3670
5574
  messages.length = 0;
@@ -3732,39 +5636,40 @@ async function startAgentReplInWorkspace(options = {}) {
3732
5636
  if (sessionArg.startsWith("resume ")) {
3733
5637
  const resumeId = sessionArg.slice(7).trim();
3734
5638
  try {
3735
- if (useJsonl && sessionExists(resumeId)) {
3736
- try {
3737
- const { verifySession } =
3738
- await import("../harness/jsonl-session-store.js");
3739
- const trust = verifySession(resumeId);
3740
- if (trust.status === "tampered") {
3741
- logger.warn(
3742
- `⚠ Transcript integrity check FAILED (${trust.reason}) — restored context is untrusted. Run 'cc session verify ${resumeId}'.`,
3743
- );
3744
- }
3745
- } catch (_err) {
3746
- // verification is best-effort in the interactive path
3747
- }
3748
- const rebuilt = rebuildMessages(resumeId);
3749
- messages.length = 1; // keep system prompt
3750
- messages.push(...rebuilt.filter((m) => m.role !== "system"));
3751
- _sanitizeRolesNextTurn =
3752
- messages[messages.length - 1]?.role === "user";
3753
- sessionId = resumeId;
3754
- // The producer owns one immutable session id. Recreate it lazily
3755
- // on the next turn so bindings from the resumed conversation can
3756
- // never be appended to the previously active session.
3757
- _turnBindingProducer = null;
3758
- _turnBindingCriticalError = null;
3759
- // The prior session's checkpoint marks (turn-index → checkpoint id)
3760
- // no longer map to this swapped-in history; keeping them would make
3761
- // a later /rewind restore files from the WRONG session's snapshot.
3762
- // Same reset /clear does on a context swap.
3763
- _checkpointMarks.length = 0;
3764
- _clearedConversation = null; // stash belongs to the swapped-out session
3765
- logger.info(
3766
- `Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
5639
+ if (useJsonl) {
5640
+ const prepared = _prepareJsonlResumeCandidate(resumeId);
5641
+ if (!prepared.ok) throw prepared.error;
5642
+ const preparedMcpRuntime = _prepareMcpHostRuntime(
5643
+ prepared.sessionId,
5644
+ prepared.mcp,
3767
5645
  );
5646
+ const preparedMcpCommit = _prepareMcpRecoveryCommit(prepared.mcp);
5647
+ const hostSystemMessages = _refreshHostSystemMessages();
5648
+ const previousState = _captureResumeState();
5649
+ const preparedState = Object.freeze({
5650
+ sessionId: prepared.sessionId,
5651
+ hostSystemMessages,
5652
+ canonicalSystemMessages: prepared.canonicalSystemMessages,
5653
+ conversationMessages: prepared.conversationMessages,
5654
+ mcpCommit: preparedMcpCommit,
5655
+ mcpRuntime: preparedMcpRuntime,
5656
+ sanitizeRolesNextTurn:
5657
+ prepared.conversationMessages.at(-1)?.role === "user",
5658
+ logMessage: `Resumed JSONL session ${prepared.sessionId} (${prepared.rebuiltMessages.length} messages)`,
5659
+ });
5660
+ const committed = commitPreparedReplJsonlResume(
5661
+ prepared,
5662
+ () => _applyPreparedResumeState(preparedState),
5663
+ () => _restoreResumeState(previousState),
5664
+ );
5665
+ if (!committed) {
5666
+ const error = new Error(
5667
+ "Prepared JSONL session resume was not committed",
5668
+ );
5669
+ error.code = "CC_REPL_SESSION_RESUME_NOT_COMMITTED";
5670
+ throw error;
5671
+ }
5672
+ _replCompactPersistence.replace(prepared.rebuiltMessages);
3768
5673
  } else if (db) {
3769
5674
  const existing = getSession(db, resumeId);
3770
5675
  if (existing && existing.messages) {
@@ -3772,18 +5677,35 @@ async function startAgentReplInWorkspace(options = {}) {
3772
5677
  typeof existing.messages === "string"
3773
5678
  ? JSON.parse(existing.messages)
3774
5679
  : existing.messages;
3775
- messages.length = 1; // keep system prompt
3776
- messages.push(...parsed.filter((m) => m.role !== "system"));
3777
- _sanitizeRolesNextTurn =
3778
- messages[messages.length - 1]?.role === "user";
3779
- sessionId = existing.id;
3780
- _turnBindingProducer = null;
3781
- _turnBindingCriticalError = null;
3782
- // Drop the prior session's checkpoint marks (see JSONL branch).
3783
- _checkpointMarks.length = 0;
3784
- _clearedConversation = null; // stash belongs to the swapped-out session
3785
- logger.info(
3786
- `Resumed session ${sessionId} (${parsed.length} messages)`,
5680
+ const rebuiltMessages = snapshotReplMessages(parsed);
5681
+ const replayMessages = Object.freeze(
5682
+ rebuiltMessages.filter((message) => message.role !== "system"),
5683
+ );
5684
+ const preparedMcpRuntime = _prepareMcpHostRuntime(existing.id, {
5685
+ recovery: null,
5686
+ recoveryError: null,
5687
+ });
5688
+ const hostSystemMessages = _refreshHostSystemMessages();
5689
+ const previousState = _captureResumeState();
5690
+ const preparedState = Object.freeze({
5691
+ sessionId: existing.id,
5692
+ hostSystemMessages,
5693
+ canonicalSystemMessages: Object.freeze([]),
5694
+ conversationMessages: replayMessages,
5695
+ mcpCommit: Object.freeze({
5696
+ recovery: null,
5697
+ recoveryError: null,
5698
+ noticeMessage: null,
5699
+ warning: null,
5700
+ }),
5701
+ mcpRuntime: preparedMcpRuntime,
5702
+ sanitizeRolesNextTurn: replayMessages.at(-1)?.role === "user",
5703
+ logMessage: `Resumed session ${existing.id} (${rebuiltMessages.length} messages)`,
5704
+ });
5705
+ commitPreparedReplDbResume(
5706
+ preparedState,
5707
+ () => _applyPreparedResumeState(preparedState),
5708
+ () => _restoreResumeState(previousState),
3787
5709
  );
3788
5710
  } else {
3789
5711
  logger.info(`Session not found: ${resumeId}`);
@@ -3792,7 +5714,14 @@ async function startAgentReplInWorkspace(options = {}) {
3792
5714
  logger.info("No session store available");
3793
5715
  }
3794
5716
  } catch (err) {
3795
- logger.error(`Resume failed: ${err.message}`);
5717
+ logger.error(
5718
+ JSON.stringify({
5719
+ type: "session.resume.refused",
5720
+ code: err?.code || "CC_REPL_SESSION_RESUME_FAILED",
5721
+ sessionId: resumeId,
5722
+ message: err?.message || "Session resume failed",
5723
+ }),
5724
+ );
3796
5725
  }
3797
5726
  } else {
3798
5727
  logger.info(`Session ID: ${sessionId || "none"}`);
@@ -3841,15 +5770,30 @@ async function startAgentReplInWorkspace(options = {}) {
3841
5770
 
3842
5771
  // Re-merge plugin hooks onto the user's settings hooks (live session).
3843
5772
  try {
3844
- const { loadHooks } = await import("../lib/settings-hooks.cjs");
5773
+ const { loadHooks, attachAuthorityErrors } =
5774
+ await import("../lib/settings-hooks.cjs");
3845
5775
  const { mergePluginHooks } =
3846
5776
  await import("../lib/plugin-runtime/hooks.js");
3847
- const base = loadHooks({ cwd }).hooks;
3848
- const merged = mergePluginHooks(base, { cwd });
5777
+ const loaded = loadHooks({ cwd });
5778
+ const merged = mergePluginHooks(
5779
+ attachAuthorityErrors(loaded.hooks, loaded.authorityErrors),
5780
+ { cwd },
5781
+ );
3849
5782
  _settingsHooks =
3850
- merged && Object.keys(merged).length > 0 ? merged : null;
3851
- } catch {
3852
- /* hooks re-merge is best-effort */
5783
+ Object.keys(merged).length > 0 || merged._authorityErrors.length > 0
5784
+ ? merged
5785
+ : null;
5786
+ } catch (error) {
5787
+ _settingsHooks = {};
5788
+ Object.defineProperty(_settingsHooks, "_authorityErrors", {
5789
+ value: Object.freeze([
5790
+ Object.freeze({
5791
+ sourceFile: null,
5792
+ code: error?.code || "CC_HOOK_AUTHORITY_LOAD_FAILED",
5793
+ }),
5794
+ ]),
5795
+ enumerable: false,
5796
+ });
3853
5797
  }
3854
5798
 
3855
5799
  // settings.json ConfigChange hooks (Claude-Code parity): the live
@@ -4497,6 +6441,31 @@ async function startAgentReplInWorkspace(options = {}) {
4497
6441
  "Plan has no items yet. Let the AI analyze the task first.",
4498
6442
  );
4499
6443
  } else {
6444
+ // Review this immutable plan version before approval. Advisor output
6445
+ // cannot block or approve the plan and grants no extra authority; its
6446
+ // local-verification checklist is queued for the next main turn.
6447
+ if (_advisorRuntime?.status().enabled) {
6448
+ try {
6449
+ const result = await _advisorRuntime.reviewPlan(
6450
+ {
6451
+ id: planManager.currentPlan.id,
6452
+ title: planManager.currentPlan.title,
6453
+ description: planManager.currentPlan.description,
6454
+ summary: planManager.generatePlanSummary(),
6455
+ },
6456
+ { messages, signal: _turnAbort?.signal || null },
6457
+ );
6458
+ if (result.ok) {
6459
+ const { renderAdvisorAdvice } =
6460
+ await import("./advisor-command.js");
6461
+ logger.log(renderAdvisorAdvice(result));
6462
+ } else if (result.effect !== "disabled") {
6463
+ logger.warn(result.error);
6464
+ }
6465
+ } catch (error) {
6466
+ logger.warn(`Advisor plan review skipped: ${error.message}`);
6467
+ }
6468
+ }
4500
6469
  planManager.approvePlan();
4501
6470
  logger.success(
4502
6471
  `Plan approved! ${planManager.currentPlan.items.length} items ready for execution.`,
@@ -4673,7 +6642,7 @@ async function startAgentReplInWorkspace(options = {}) {
4673
6642
 
4674
6643
  // `/mcp` — overview of connected MCP servers' resources + prompts.
4675
6644
  if (trimmed === "/mcp" || trimmed.startsWith("/mcp ")) {
4676
- const mcpClient = _adhocMcp?.mcpClient || _bundleMcpClient;
6645
+ const mcpClient = _getReplHostMcp()?.mcpClient;
4677
6646
  logger.log(renderMcpSurface(mcpClient));
4678
6647
  prompt();
4679
6648
  return;
@@ -4700,7 +6669,12 @@ async function startAgentReplInWorkspace(options = {}) {
4700
6669
  } else if (cmd.action === "help") {
4701
6670
  logger.log(renderConfigHelp());
4702
6671
  } else if (cmd.action === "get") {
4703
- logger.log(renderConfigGet(cmd.key, cm.getConfigValue(cmd.key)));
6672
+ logger.log(
6673
+ renderConfigGet(
6674
+ cmd.key,
6675
+ cm.getConfigValue(cmd.key, { resolveSecrets: false }),
6676
+ ),
6677
+ );
4704
6678
  } else if (cmd.action === "set") {
4705
6679
  cm.setConfigValue(cmd.key, cmd.value);
4706
6680
  logger.log(
@@ -5112,7 +7086,7 @@ async function startAgentReplInWorkspace(options = {}) {
5112
7086
  try {
5113
7087
  const expanded = await expandMcpPrompt(
5114
7088
  promptText,
5115
- _adhocMcp?.mcpClient || _bundleMcpClient,
7089
+ _getReplHostMcp()?.mcpClient,
5116
7090
  );
5117
7091
  if (expanded != null) {
5118
7092
  promptText = expanded;
@@ -5185,7 +7159,7 @@ async function startAgentReplInWorkspace(options = {}) {
5185
7159
  }
5186
7160
 
5187
7161
  // Backstop: if the previous turn's agentLoop threw before the success-path
5188
- // restore ran, undo its /btw aside now so it never leaks into this turn's
7162
+ // restore ran, undo its /note-next guidance so it never leaks into this turn's
5189
7163
  // history/model call. (Normal turns clear _btwRestore right after agentLoop.)
5190
7164
  if (_btwRestore) {
5191
7165
  _btwRestore.msg.content = _btwRestore.content;
@@ -5335,11 +7309,12 @@ async function startAgentReplInWorkspace(options = {}) {
5335
7309
  try {
5336
7310
  const { buildIdePromptContext, expandIdeMentions } =
5337
7311
  await import("../lib/ide-context.js");
5338
- const ideCtx = await buildIdePromptContext(_adhocMcp);
7312
+ const hostMcp = _getReplHostMcp();
7313
+ const ideCtx = await buildIdePromptContext(hostMcp);
5339
7314
  if (ideCtx) userContent += `\n\n${ideCtx}`;
5340
7315
  // Explicit @selection / @diagnostics mentions (Claude-Code parity);
5341
7316
  // scan the user's original prompt, append the expansion ephemerally.
5342
- const mentioned = await expandIdeMentions(effectivePrompt, _adhocMcp);
7317
+ const mentioned = await expandIdeMentions(effectivePrompt, hostMcp);
5343
7318
  for (const w of mentioned.warnings) {
5344
7319
  logger.info(chalk.yellow(`[@ide] ${w}`));
5345
7320
  }
@@ -5389,7 +7364,7 @@ async function startAgentReplInWorkspace(options = {}) {
5389
7364
  return;
5390
7365
  }
5391
7366
 
5392
- // Add user message (keep the object ref so a /btw aside can be injected for
7367
+ // Add user message (keep the object ref so /note-next can be injected for
5393
7368
  // this turn's model call and then stripped before persistence).
5394
7369
  const _userMsg = { role: "user", content: _userMessageContent };
5395
7370
  messages.push(_userMsg);
@@ -5529,6 +7504,28 @@ async function startAgentReplInWorkspace(options = {}) {
5529
7504
  /* goal binding is best-effort — fall back to defaultPrepareCall */
5530
7505
  }
5531
7506
  _turnAbort = new AbortController();
7507
+ if (_advisorRuntime) {
7508
+ _advisorRuntime.beginTurn(
7509
+ `${sessionId || "ephemeral"}:${_turnCount + 1}`,
7510
+ );
7511
+ prepareCall = _advisorRuntime.createPrepareCall({
7512
+ messages,
7513
+ basePrepareCall: prepareCall,
7514
+ subject: promptText,
7515
+ signal: _turnAbort.signal,
7516
+ onAdvice: async (result) => {
7517
+ if (!result.ok) {
7518
+ if (result.effect !== "disabled") {
7519
+ process.stderr.write(` Advisor: ${result.error}\n`);
7520
+ }
7521
+ return;
7522
+ }
7523
+ const { renderAdvisorAdvice } =
7524
+ await import("./advisor-command.js");
7525
+ process.stderr.write(`\n${renderAdvisorAdvice(result)}\n`);
7526
+ },
7527
+ });
7528
+ }
5532
7529
  // Live streaming hooks: write the answer token-by-token, and stream the
5533
7530
  // reasoning dimmed before it. Skipped (left undefined) in replay mode.
5534
7531
  let _liveStreamed = false;
@@ -5553,7 +7550,7 @@ async function startAgentReplInWorkspace(options = {}) {
5553
7550
  }
5554
7551
  : {};
5555
7552
  if (_streamLive) process.stdout.write("\n");
5556
- // Inject any queued /btw asides into THIS turn's user message just before
7553
+ // Inject queued /note-next guidance into THIS turn just before
5557
7554
  // the model call. We remember the pre-aside content so it's restored right
5558
7555
  // after agentLoop returns — the aside steers this answer but is never
5559
7556
  // persisted (saveMessages/JSONL) or carried into later turns. Consumed on
@@ -5565,7 +7562,7 @@ async function startAgentReplInWorkspace(options = {}) {
5565
7562
  _btwRestore = { msg: _userMsg, content: _userMsg.content };
5566
7563
  _userMsg.content = applyAside(_userMsg.content, block);
5567
7564
  logger.verbose(
5568
- `[btw] applied ${pendingBtw.length} aside(s) to this turn`,
7565
+ `[note-next] applied ${pendingBtw.length} note(s) to this turn`,
5569
7566
  );
5570
7567
  }
5571
7568
  pendingBtw = [];
@@ -5582,6 +7579,8 @@ async function startAgentReplInWorkspace(options = {}) {
5582
7579
  await import("../lib/async-hook-supervisor.js");
5583
7580
  _asyncHookSupervisor = new AsyncHookSupervisor({ persistStats: true });
5584
7581
  }
7582
+ const activeMcpRuntime = _activateMcpHostRuntime();
7583
+ const activeRawMcpClient = activeMcpRuntime.rawClient || undefined;
5585
7584
  const {
5586
7585
  content: response,
5587
7586
  usageEvents,
@@ -5653,6 +7652,9 @@ async function startAgentReplInWorkspace(options = {}) {
5653
7652
  sandbox: _sandbox,
5654
7653
  autoCheckpoint,
5655
7654
  checkpointSession: sessionId,
7655
+ managedCheckpoint,
7656
+ managedCheckpointStateDir,
7657
+ managedCheckpointExclusions,
5656
7658
  checkpointMarks: _checkpointMarks,
5657
7659
  // Explicit turn-binding producer (null unless a JSONL session): the
5658
7660
  // wrapper folds every loop event into the live table.
@@ -5683,16 +7685,21 @@ async function startAgentReplInWorkspace(options = {}) {
5683
7685
  interactiveApproval: true,
5684
7686
  // MCP: --mcp-config (ad-hoc) wins; bundle MCP is the fallback. The 3
5685
7687
  // tool channels expose --mcp-config servers' tools to the LLM directly.
5686
- mcpClient: _adhocMcp?.mcpClient || _bundleMcpClient || undefined,
7688
+ mcpClient: activeRawMcpClient,
7689
+ mcpHostClient: activeMcpRuntime.runtime.client,
5687
7690
  extraToolDefinitions: _adhocMcp?.extraToolDefinitions,
5688
7691
  externalToolExecutors: _adhocMcp?.externalToolExecutors,
5689
7692
  externalToolDescriptors: _adhocMcp?.externalToolDescriptors,
7693
+ // The recovery controller is required even when this legacy/ephemeral
7694
+ // session has no durable sink. Otherwise an outcome-unknown call could
7695
+ // be retried in the same process after agent-core creates a plain ledger.
7696
+ mcpCallLedger: activeMcpRuntime.runtime.ledger,
5690
7697
  chatFn: _fallbackChatFn,
5691
7698
  });
5692
7699
  _persistReplContextSources();
5693
7700
  _turnAbort = null;
5694
7701
 
5695
- // Strip the one-shot /btw aside now the model has seen it — so it is never
7702
+ // Strip one-shot /note-next guidance now the model has seen it so it is never
5696
7703
  // persisted (DB saveMessages below) or carried into the next turn.
5697
7704
  if (_btwRestore) {
5698
7705
  _btwRestore.msg.content = _btwRestore.content;
@@ -5867,8 +7874,16 @@ async function startAgentReplInWorkspace(options = {}) {
5867
7874
  if (useJsonl) {
5868
7875
  // Append incremental events (user + assistant)
5869
7876
  appendUserMessage(sessionId, effectivePrompt);
7877
+ _replCompactPersistence.record({
7878
+ role: "user",
7879
+ content: effectivePrompt,
7880
+ });
5870
7881
  if (effectiveResponse) {
5871
7882
  appendAssistantMessage(sessionId, effectiveResponse);
7883
+ _replCompactPersistence.record({
7884
+ role: "assistant",
7885
+ content: effectiveResponse,
7886
+ });
5872
7887
  }
5873
7888
  } else if (db) {
5874
7889
  saveMessages(db, sessionId, messages);
@@ -5903,16 +7918,21 @@ async function startAgentReplInWorkspace(options = {}) {
5903
7918
  logger.verbose(
5904
7919
  `Auto-compacted: ${stats.strategy} (saved ${stats.saved} tokens)`,
5905
7920
  );
7921
+ if (stats.degraded === true) {
7922
+ logger.warn(
7923
+ `Auto-compaction degraded to ${stats.summaryMode}: ${stats.degradedReason}`,
7924
+ );
7925
+ }
5906
7926
  // Write compact checkpoint to JSONL for crash recovery
5907
7927
  if (useJsonl && sessionId) {
5908
- appendCompactEvent(sessionId, {
7928
+ _replCompactPersistence.persist(sessionId, {
5909
7929
  ...stats,
5910
7930
  messages: compacted,
5911
7931
  });
5912
7932
  }
5913
7933
  }
5914
7934
  } catch (_e) {
5915
- // Non-critical — continue with uncompacted messages
7935
+ logger.warn(`Auto-compaction failed: ${_e.message}`);
5916
7936
  }
5917
7937
  }
5918
7938
 
@@ -5993,6 +8013,15 @@ async function startAgentReplInWorkspace(options = {}) {
5993
8013
 
5994
8014
  rl.on("line", async (input) => {
5995
8015
  if (_processingLine) {
8016
+ const concurrentBtw = _slashCommandsDisabled
8017
+ ? null
8018
+ : parseBtwCommand(input.trim());
8019
+ if (concurrentBtw) {
8020
+ // Intentionally do not await: the main turn keeps streaming while this
8021
+ // independent, tool-free snapshot call runs alongside it.
8022
+ void runBtwSideQuestion(concurrentBtw, { concurrent: true });
8023
+ return;
8024
+ }
5996
8025
  if (input.trim()) {
5997
8026
  _pendingLines.push(input);
5998
8027
  logger.log(
@@ -6057,7 +8086,7 @@ async function startAgentReplInWorkspace(options = {}) {
6057
8086
  if (useJsonl) {
6058
8087
  _persistReplContextSources();
6059
8088
  // JSONL: write final compact snapshot for fast rebuild
6060
- appendCompactEvent(sessionId, {
8089
+ _replCompactPersistence.persist(sessionId, {
6061
8090
  strategy: "session-end",
6062
8091
  messages,
6063
8092
  });
@@ -6065,7 +8094,7 @@ async function startAgentReplInWorkspace(options = {}) {
6065
8094
  saveMessages(db, sessionId, messages);
6066
8095
  }
6067
8096
  } catch (_e) {
6068
- // Non-critical
8097
+ logger.warn(`Session-end compact not persisted: ${_e.message}`);
6069
8098
  }
6070
8099
  }
6071
8100
  // Auto-summarize session into permanent memory