chainlesschain 0.162.95 → 0.162.97

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 (219) hide show
  1. package/README.md +1 -1
  2. package/bin/chainlesschain.js +8 -18
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DLvvkb9x.js → AIOps-C1YvIYh5.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-jqcXEakj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-C8Xwqd3_.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-Dx5ypTfA.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-1GZ4Tv2f.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CexR2hyk.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-DwAruKdy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-W2FkU5BY.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-BCLnhLfu.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Dkh5JY5B.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-LfYOgsdA.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-DEgketAC.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-LeFQMaZm.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CrlKYvBc.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-ca_4PGYF.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-BcMWqh2a.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-pVVEA3dw.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-CiKSGTqp.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID-B2bGzdOj.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-sQ6jcgCn.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-rFx_8P04.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-smw2hh6f.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-CZhg2_S2.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-DQE-368G.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-2XPb9izr.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-DCGMz5E6.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-BFwrxK4Q.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-ChxBRwG1.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-C7kyknu_.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-BsMd4XAt.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Bs3yrpow.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-Cc29jdBv.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-zYpYsUsl.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-x9r4yKtS.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-CjUo8prx.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-Cmz4wVrF.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-Y-SBnNxm.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-ogiKnrxj.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C1hygkFR.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-fWWYiTsY.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-CkCNF4gC.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-DThB7WQG.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-Dh64aMq6.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-BSY16zvP.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-C_TDh18K.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CvxZk6og.js} +2 -2
  50. package/src/assets/web-panel/assets/PdhVaultBrowser-0JZ5JF4M.js +7 -0
  51. package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-DV8ubpjM.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-g2Dgls8e.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-DGp8zQ0C.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-C0uHrjWR.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-2XGICaFt.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Cfy72e0Q.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-CXydUXp8.js → Projects-Xsvk_kKT.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-vuvDVg7U.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-BUy7kPyQ.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-DlzpC_JE.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-BiWFwPMh.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-dmfZBK-k.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-Cf6tlRKq.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DhzXDRAJ.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-C-bTfgeJ.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-D3KNwBqu.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CHH8i1xY.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-DHiONX0z.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DwWMb-Ui.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-DAq64iWB.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-DJU-kHko.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-RAIzA3u-.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-By4cbadT.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-kCAXsxWu.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-D8pup_CO.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-Bk2c9TUI.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-B0wTiZRa.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-Ca_9t0gk.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-BnfNDDDm.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-fnlF2-Yr.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-Cfshhzyw.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-BWMpDOLG.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn-B6xeXZvw.js} +3 -3
  84. package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-Cb3-FPCE.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CP_opuMK.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-RLzfjxRQ.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-BTRloNKV.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-gXgXTLlU.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-CYlmApo_.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-CrIpxkRC.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-AsI1SqnE.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B3L5vTSs.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-B5sN_pwd.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-BEcRPNHv.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BVrcoJe2.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BewxG36A.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-Bi87H9yk.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-Bl4OQ--4.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-yavsyHif.js → index-BlE2lCS-.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-BlS91Ac0.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-Bm0cNeeQ.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-Bo2Fl2j7.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-Bsz3X-aU.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DOYcemym.js → index-BurvDWMS.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-BzBLtthb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-C0wmEN2j.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-C4bBR-Vd.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-COhACL0I.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-CQ-cnhZM.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-CRr99L7q.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-CaTkUkJp.js} +3 -3
  112. package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CfOjPxso.js} +1 -1
  113. package/src/assets/web-panel/assets/index-Cf_SLgtj.js +1 -0
  114. package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-Ch_TYQNW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CisGGaF3.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-DA-_2qTs.js} +1 -1
  117. package/src/assets/web-panel/assets/index-DYBIDVGn.js +1 -0
  118. package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-DZxK7MW9.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DiXnGU0A.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-DsiyfyBE.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-G_OgtHiC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-IjV-dnE8.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-Mltfj22U.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-U-kmEJdu.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-cgf-OVry.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-ch8zcf-h.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-cqOT5RiJ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-C7sBLech.js → index-cywkZJ-I.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-sE_I7gVL.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-DMwnVZzH.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-Dnpjpvo7.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-iuRh5U9Y.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-Bl4WRV59.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-B9KSXaep.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-TEGIow4r.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve--BECM92v.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-Ca8G7XbL.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-8QPG_T3D.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-p3FsA6mg.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-Ct0yIzrp.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-3nlByl_3.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-N3OsoSRM.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-Cfe3ywq3.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-DVBuDS_3.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/activitypub.js +4 -3
  147. package/src/commands/agent.js +25 -0
  148. package/src/commands/agents.js +18 -4
  149. package/src/commands/ask.js +13 -0
  150. package/src/commands/command.js +12 -2
  151. package/src/commands/config.js +17 -1
  152. package/src/commands/cost.js +6 -1
  153. package/src/commands/dao.js +4 -3
  154. package/src/commands/dlp.js +2 -1
  155. package/src/commands/hub.js +138 -0
  156. package/src/commands/mtc.js +37 -1
  157. package/src/gateways/discord/discord-formatter.js +18 -1
  158. package/src/gateways/telegram/telegram-formatter.js +6 -3
  159. package/src/gateways/ws/ws-server.js +19 -1
  160. package/src/harness/plugin-manager.js +20 -0
  161. package/src/harness/worktree-isolator.js +17 -1
  162. package/src/index.js +2 -0
  163. package/src/lib/__tests__/model-deprecation.test.js +194 -0
  164. package/src/lib/a2a-protocol.js +24 -9
  165. package/src/lib/agent-core.js +4 -5
  166. package/src/lib/audit-mtc.js +40 -2
  167. package/src/lib/chat-core.js +146 -18
  168. package/src/lib/cli-anything-bridge.js +5 -0
  169. package/src/lib/cli-numeric.js +38 -0
  170. package/src/lib/config-manager.js +37 -6
  171. package/src/lib/cowork/ab-comparator-cli.js +6 -2
  172. package/src/lib/cowork-cron.js +45 -34
  173. package/src/lib/cowork-learning.js +31 -1
  174. package/src/lib/cowork-share.js +39 -3
  175. package/src/lib/cowork-template-marketplace.js +54 -2
  176. package/src/lib/cowork-workflow.js +33 -6
  177. package/src/lib/cross-chain-mtc.js +41 -12
  178. package/src/lib/crypto-manager.js +12 -2
  179. package/src/lib/dbevo.js +19 -8
  180. package/src/lib/downloader.js +24 -9
  181. package/src/lib/evomap-governance.js +4 -4
  182. package/src/lib/evomap-manager.js +54 -7
  183. package/src/lib/fatal-handler.js +48 -0
  184. package/src/lib/file-checkpoint.js +26 -0
  185. package/src/lib/git-integration.js +28 -0
  186. package/src/lib/goal-store.js +36 -10
  187. package/src/lib/hierarchical-memory.js +6 -0
  188. package/src/lib/llm-pricing.js +52 -2
  189. package/src/lib/mcp-oauth.js +37 -7
  190. package/src/lib/memory-manager.js +23 -2
  191. package/src/lib/model-deprecation.js +249 -0
  192. package/src/lib/pdh-feedback-ledger.js +131 -0
  193. package/src/lib/permanent-memory.js +30 -2
  194. package/src/lib/permission-rules.cjs +11 -3
  195. package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
  196. package/src/lib/personal-data-hub-wiring.js +39 -16
  197. package/src/lib/project-instructions.js +10 -4
  198. package/src/lib/runnable-provider.js +62 -31
  199. package/src/lib/session-usage.js +34 -1
  200. package/src/lib/skill-loader.js +4 -1
  201. package/src/lib/skill-packs/generator.js +25 -2
  202. package/src/lib/stream-retry.js +56 -0
  203. package/src/lib/sync-credentials.js +53 -17
  204. package/src/lib/token-tracker.js +12 -2
  205. package/src/lib/with-file-lock.js +87 -0
  206. package/src/repl/agent-repl.js +47 -7
  207. package/src/repl/chat-repl.js +19 -0
  208. package/src/repl/session-cost.js +31 -3
  209. package/src/runtime/agent-core.js +478 -53
  210. package/src/runtime/coding-agent-contract-shared.cjs +58 -1
  211. package/src/runtime/coding-agent-policy.cjs +10 -0
  212. package/src/runtime/file-ref-expander.js +29 -1
  213. package/src/runtime/headless-runner.js +12 -1
  214. package/src/runtime/headless-stream.js +200 -9
  215. package/src/skills/video-editing/tools/commit.js +15 -1
  216. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
  217. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
  218. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
  219. package/src/assets/web-panel/assets/index-N_oOFphx.js +0 -1
@@ -43,6 +43,11 @@ import {
43
43
  import { createToolContext } from "../tools/tool-context.js";
44
44
  import { createToolTelemetryRecord } from "../tools/tool-telemetry.js";
45
45
  import { isAbortError, throwIfAborted } from "../lib/abort-utils.js";
46
+ import {
47
+ isRetryableStreamError,
48
+ STREAM_RETRY_MAX,
49
+ STREAM_RETRY_BASE_MS,
50
+ } from "../lib/stream-retry.js";
46
51
  import {
47
52
  annotateLines,
48
53
  replaceByHash,
@@ -154,9 +159,20 @@ function _killTask(task) {
154
159
  try {
155
160
  if (process.platform === "win32") {
156
161
  if (child.pid) {
157
- spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
162
+ const tk = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
158
163
  windowsHide: true,
159
164
  });
165
+ // A spawn that fails to launch emits an async 'error' event — with no
166
+ // listener Node rethrows it as an UNCAUGHT exception (the try/catch
167
+ // here can't catch the async emit), crashing the CLI. Handle it and
168
+ // fall back to a direct kill.
169
+ tk.on("error", () => {
170
+ try {
171
+ child.kill();
172
+ } catch {
173
+ /* already dead */
174
+ }
175
+ });
160
176
  } else {
161
177
  child.kill();
162
178
  }
@@ -684,7 +700,7 @@ export function computeProposedEdit(name, args = {}, cwd = process.cwd()) {
684
700
  }
685
701
  return {
686
702
  filePath,
687
- newContent: content.replace(args.old_string, args.new_string),
703
+ newContent: content.replace(args.old_string, () => args.new_string),
688
704
  originalText: content,
689
705
  };
690
706
  }
@@ -953,7 +969,9 @@ export async function executeTool(name, args, context = {}) {
953
969
  // An explicit settings `allow` rule is the only bypass (exact user
954
970
  // pre-authorization); headless without a confirmer fails closed.
955
971
  if (
956
- (name === "write_file" || name === "edit_file") &&
972
+ (name === "write_file" ||
973
+ name === "edit_file" ||
974
+ name === "notebook_edit") &&
957
975
  settingsVerdict.decision !== "allow" &&
958
976
  args?.path
959
977
  ) {
@@ -1212,7 +1230,8 @@ export async function executeTool(name, args, context = {}) {
1212
1230
  if (
1213
1231
  (name === "write_file" ||
1214
1232
  name === "edit_file" ||
1215
- name === "edit_file_hashed") &&
1233
+ name === "edit_file_hashed" ||
1234
+ name === "notebook_edit") &&
1216
1235
  toolResult &&
1217
1236
  typeof toolResult === "object" &&
1218
1237
  !toolResult.error &&
@@ -1271,6 +1290,173 @@ export function writeFileVerified(filePath, content, fsImpl = fs) {
1271
1290
  return { size: actual };
1272
1291
  }
1273
1292
 
1293
+ // Short, stable-ish id for a freshly inserted notebook cell (nbformat 4.5+).
1294
+ function _newNotebookCellId() {
1295
+ return Math.random().toString(36).slice(2, 10);
1296
+ }
1297
+
1298
+ /**
1299
+ * Apply a single cell edit to a Jupyter notebook (.ipynb) given its raw text.
1300
+ * Pure (no fs) + exported for tests. Returns `{ text, summary, cellId }` on
1301
+ * success or `{ error }` on any validation failure — the caller writes `text`.
1302
+ *
1303
+ * edit_mode:
1304
+ * - "replace" (default): overwrite the target cell's source; a code cell also
1305
+ * gets outputs=[] + execution_count=null (stale output would be misleading).
1306
+ * - "insert": add a new cell of `cell_type` after the target (or at the top
1307
+ * when no target is given).
1308
+ * - "delete": remove the target cell.
1309
+ * Target = cell_id (preferred) or 0-based cell_index.
1310
+ */
1311
+ export function editNotebookCell(notebookText, args = {}) {
1312
+ let nb;
1313
+ try {
1314
+ nb = JSON.parse(notebookText);
1315
+ } catch {
1316
+ return { error: "Notebook is not valid JSON (.ipynb)" };
1317
+ }
1318
+ if (!nb || !Array.isArray(nb.cells)) {
1319
+ return { error: "Not a valid notebook: missing cells[] (need nbformat 4)" };
1320
+ }
1321
+ const mode = args.edit_mode || "replace";
1322
+ if (!["replace", "insert", "delete"].includes(mode)) {
1323
+ return { error: `Unknown edit_mode "${mode}" (replace|insert|delete)` };
1324
+ }
1325
+ // .ipynb stores source as an array of lines that each KEEP their trailing \n.
1326
+ const toLines = (s) => {
1327
+ const t = String(s ?? "");
1328
+ return t === "" ? [] : t.split(/(?<=\n)/);
1329
+ };
1330
+ // Re-serialize with Jupyter's 1-space indent + trailing newline.
1331
+ const out = (summary, cellId) => ({
1332
+ text: JSON.stringify(nb, null, 1) + "\n",
1333
+ summary,
1334
+ cellId,
1335
+ });
1336
+ const locate = () => {
1337
+ if (args.cell_id != null) {
1338
+ return nb.cells.findIndex((c) => c && c.id === args.cell_id);
1339
+ }
1340
+ if (Number.isInteger(args.cell_index)) {
1341
+ return args.cell_index >= 0 && args.cell_index < nb.cells.length
1342
+ ? args.cell_index
1343
+ : -1;
1344
+ }
1345
+ return -2; // no locator supplied
1346
+ };
1347
+
1348
+ if (mode === "insert") {
1349
+ if (typeof args.new_source !== "string") {
1350
+ return { error: "new_source is required for edit_mode 'insert'" };
1351
+ }
1352
+ const ct = args.cell_type;
1353
+ if (ct !== "code" && ct !== "markdown") {
1354
+ return {
1355
+ error:
1356
+ "cell_type ('code'|'markdown') is required for edit_mode 'insert'",
1357
+ };
1358
+ }
1359
+ const id = _newNotebookCellId();
1360
+ const cell =
1361
+ ct === "code"
1362
+ ? {
1363
+ cell_type: "code",
1364
+ id,
1365
+ metadata: {},
1366
+ source: toLines(args.new_source),
1367
+ outputs: [],
1368
+ execution_count: null,
1369
+ }
1370
+ : {
1371
+ cell_type: "markdown",
1372
+ id,
1373
+ metadata: {},
1374
+ source: toLines(args.new_source),
1375
+ };
1376
+ let at = 0;
1377
+ if (args.cell_id != null || Number.isInteger(args.cell_index)) {
1378
+ const idx = locate();
1379
+ if (idx < 0) return { error: "Target cell not found for insert" };
1380
+ at = idx + 1; // insert AFTER the target
1381
+ }
1382
+ nb.cells.splice(at, 0, cell);
1383
+ return out(`inserted ${ct} cell at index ${at}`, id);
1384
+ }
1385
+
1386
+ // replace / delete both need an existing target
1387
+ const idx = locate();
1388
+ if (idx === -2) {
1389
+ return {
1390
+ error: "Provide cell_id or cell_index to identify the target cell",
1391
+ };
1392
+ }
1393
+ if (idx < 0) {
1394
+ return {
1395
+ error: `Target cell not found (${
1396
+ args.cell_id != null
1397
+ ? "cell_id " + args.cell_id
1398
+ : "cell_index " + args.cell_index
1399
+ })`,
1400
+ };
1401
+ }
1402
+
1403
+ if (mode === "delete") {
1404
+ const [removed] = nb.cells.splice(idx, 1);
1405
+ return out(`deleted cell at index ${idx}`, removed?.id);
1406
+ }
1407
+
1408
+ // replace
1409
+ if (typeof args.new_source !== "string") {
1410
+ return { error: "new_source is required for edit_mode 'replace'" };
1411
+ }
1412
+ const cell = nb.cells[idx];
1413
+ cell.source = toLines(args.new_source);
1414
+ if (cell.cell_type === "code") {
1415
+ cell.outputs = [];
1416
+ cell.execution_count = null;
1417
+ }
1418
+ return out(`replaced ${cell.cell_type} cell at index ${idx}`, cell.id);
1419
+ }
1420
+
1421
+ /**
1422
+ * Render a Jupyter notebook (.ipynb) as a compact, token-cheap cell listing so
1423
+ * the model can locate cells (index + id + type + source) to edit with
1424
+ * notebook_edit — instead of drowning in raw JSON / base64 output blobs. Cell
1425
+ * OUTPUTS are summarized, not dumped. Pure + exported for tests; returns null
1426
+ * when the text is not a parseable nbformat-4 notebook (caller falls back to raw).
1427
+ */
1428
+ export function renderNotebook(text) {
1429
+ let nb;
1430
+ try {
1431
+ nb = JSON.parse(text);
1432
+ } catch {
1433
+ return null;
1434
+ }
1435
+ if (!nb || !Array.isArray(nb.cells)) return null;
1436
+ const srcOf = (c) =>
1437
+ Array.isArray(c.source) ? c.source.join("") : String(c.source ?? "");
1438
+ const lines = [
1439
+ `Jupyter notebook — ${nb.cells.length} cell(s), nbformat ${nb.nbformat ?? "?"}. Edit cells with the notebook_edit tool (target by the id shown below). Pass raw:true to read_file for the underlying JSON.`,
1440
+ "",
1441
+ ];
1442
+ nb.cells.forEach((c, i) => {
1443
+ const id = c && c.id != null ? ` id=${c.id}` : "";
1444
+ lines.push(`── Cell ${i} [${(c && c.cell_type) || "?"}${id}] ──`);
1445
+ const src = srcOf(c || {});
1446
+ lines.push(src.length ? src.replace(/\n$/, "") : "(empty)");
1447
+ if (
1448
+ c &&
1449
+ c.cell_type === "code" &&
1450
+ Array.isArray(c.outputs) &&
1451
+ c.outputs.length
1452
+ ) {
1453
+ lines.push(` ⟨${c.outputs.length} output(s) hidden⟩`);
1454
+ }
1455
+ lines.push("");
1456
+ });
1457
+ return lines.join("\n");
1458
+ }
1459
+
1274
1460
  /**
1275
1461
  * Inner tool execution — no hooks, no plan-mode checks.
1276
1462
  */
@@ -1340,6 +1526,24 @@ async function executeToolInner(
1340
1526
  return attachDescriptor({ error: `File not found: ${filePath}` });
1341
1527
  }
1342
1528
  const content = fs.readFileSync(filePath, "utf8");
1529
+ // Jupyter notebooks: render a compact cell listing (index/id/type/source,
1530
+ // outputs summarized) so the model can find cells for notebook_edit
1531
+ // without ingesting raw JSON / base64 output blobs. `raw:true` returns the
1532
+ // underlying JSON. Non-.ipynb reads are unchanged.
1533
+ if (args.raw !== true && /\.ipynb$/i.test(filePath)) {
1534
+ const nbView = renderNotebook(content);
1535
+ if (nbView) {
1536
+ return attachDescriptor(
1537
+ nbView.length > 50000
1538
+ ? {
1539
+ content: nbView.substring(0, 50000) + "\n...(truncated)",
1540
+ size: nbView.length,
1541
+ notebook: true,
1542
+ }
1543
+ : { content: nbView, notebook: true },
1544
+ );
1545
+ }
1546
+ }
1343
1547
  // Hashline mode: prefix each line with a 6-char content hash tag
1344
1548
  // so downstream edit_file_hashed calls can anchor by hash.
1345
1549
  const rendered = args.hashed === true ? annotateLines(content) : content;
@@ -1371,6 +1575,25 @@ async function executeToolInner(
1371
1575
  });
1372
1576
  }
1373
1577
 
1578
+ case "notebook_edit": {
1579
+ const filePath = path.resolve(cwd, args.path);
1580
+ if (!fs.existsSync(filePath)) {
1581
+ return attachDescriptor({ error: `Notebook not found: ${filePath}` });
1582
+ }
1583
+ const original = fs.readFileSync(filePath, "utf8");
1584
+ const res = editNotebookCell(original, args);
1585
+ if (res.error) return attachDescriptor({ error: res.error });
1586
+ const wrote = writeFileVerified(filePath, res.text);
1587
+ if (wrote.error) return attachDescriptor({ error: wrote.error });
1588
+ return attachDescriptor({
1589
+ success: true,
1590
+ path: filePath,
1591
+ size: wrote.size,
1592
+ summary: res.summary,
1593
+ cellId: res.cellId,
1594
+ });
1595
+ }
1596
+
1374
1597
  case "edit_file": {
1375
1598
  const filePath = path.resolve(cwd, args.path);
1376
1599
  if (!fs.existsSync(filePath)) {
@@ -1380,7 +1603,10 @@ async function executeToolInner(
1380
1603
  if (!content.includes(args.old_string)) {
1381
1604
  return attachDescriptor({ error: "old_string not found in file" });
1382
1605
  }
1383
- const newContent = content.replace(args.old_string, args.new_string);
1606
+ const newContent = content.replace(
1607
+ args.old_string,
1608
+ () => args.new_string,
1609
+ );
1384
1610
  const wrote = writeFileVerified(filePath, newContent);
1385
1611
  if (wrote.error) return attachDescriptor({ error: wrote.error });
1386
1612
  return attachDescriptor({
@@ -2679,6 +2905,9 @@ export async function chatWithTools(rawMessages, options) {
2679
2905
  { model, messages: ollamaMessages, tools },
2680
2906
  options.onToken,
2681
2907
  signal,
2908
+ options.onStall,
2909
+ options.streamStallMs,
2910
+ options.streamStallTimeoutMs,
2682
2911
  ),
2683
2912
  { signal, onRetry: options.onStreamRetry },
2684
2913
  );
@@ -2720,10 +2949,30 @@ export async function chatWithTools(rawMessages, options) {
2720
2949
  input_schema: t.function.parameters,
2721
2950
  }));
2722
2951
 
2952
+ // Prompt caching (Claude-Code parity, default-on): the system prompt + the
2953
+ // ~18 tool schemas are a large, stable prefix re-sent on every agent-loop
2954
+ // iteration. Marking the LAST tool and the system block as cache
2955
+ // breakpoints lets Anthropic serve that prefix from cache (~10% the input
2956
+ // cost, lower latency) across iterations and turns. Anthropic ignores a
2957
+ // breakpoint when the prefix is under the model's minimum cacheable size,
2958
+ // so it is always safe. Opt out (e.g. a custom gateway that rejects the
2959
+ // field) with CC_PROMPT_CACHE=0 or options.promptCaching:false.
2960
+ const cacheEnabled =
2961
+ options.promptCaching !== false && process.env.CC_PROMPT_CACHE !== "0";
2962
+ if (cacheEnabled && anthropicTools.length > 0) {
2963
+ const last = anthropicTools.length - 1;
2964
+ anthropicTools[last] = {
2965
+ ...anthropicTools[last],
2966
+ cache_control: { type: "ephemeral" },
2967
+ };
2968
+ }
2969
+
2723
2970
  // Model-aware max_tokens (Opus → 16384, Haiku → 4096, else 8192) via
2724
2971
  // provider-options. We read ONLY maxTokens: the module's `temperature`
2725
2972
  // default is never forwarded (400s on Opus 4.7/4.8).
2726
- const effModel = model || "claude-sonnet-4-20250514";
2973
+ // Fallback to the CURRENT Sonnet, not a retired snapshot:
2974
+ // claude-sonnet-4-20250514 was retired 2026-06-15 and would 404 here.
2975
+ const effModel = model || "claude-sonnet-4-6";
2727
2976
  const { maxTokens: anthropicMaxTokens } = mergeProviderOptions(
2728
2977
  "anthropic",
2729
2978
  effModel,
@@ -2750,7 +2999,18 @@ export async function chatWithTools(rawMessages, options) {
2750
2999
  );
2751
3000
  if (thinkingParams) Object.assign(body, thinkingParams);
2752
3001
  if (systemMsgs.length > 0) {
2753
- body.system = systemMsgs.map((m) => m.content).join("\n");
3002
+ const systemText = systemMsgs.map((m) => m.content).join("\n");
3003
+ // Array-form system with a cache breakpoint when caching is on; plain
3004
+ // string otherwise (byte-identical to the prior request shape).
3005
+ body.system = cacheEnabled
3006
+ ? [
3007
+ {
3008
+ type: "text",
3009
+ text: systemText,
3010
+ cache_control: { type: "ephemeral" },
3011
+ },
3012
+ ]
3013
+ : systemText;
2754
3014
  }
2755
3015
 
2756
3016
  const url =
@@ -2771,6 +3031,9 @@ export async function chatWithTools(rawMessages, options) {
2771
3031
  options.onToken,
2772
3032
  signal,
2773
3033
  options.onThinking,
3034
+ options.onStall,
3035
+ options.streamStallMs,
3036
+ options.streamStallTimeoutMs,
2774
3037
  ),
2775
3038
  { signal, onRetry: options.onStreamRetry },
2776
3039
  );
@@ -2795,8 +3058,15 @@ export async function chatWithTools(rawMessages, options) {
2795
3058
  const normalized = _normalizeAnthropicResponse(data);
2796
3059
  if (data.usage) {
2797
3060
  normalized.usage = {
3061
+ // With prompt caching, Anthropic splits input into uncached
3062
+ // `input_tokens` + cache read/write — capture all three so cost
3063
+ // accounting prices the cached prefix correctly (read ≈ 0.1×,
3064
+ // write ≈ 1.25× input). Absent (caching off) → 0, byte-identical.
2798
3065
  input_tokens: data.usage.input_tokens || 0,
2799
3066
  output_tokens: data.usage.output_tokens || 0,
3067
+ cache_read_input_tokens: data.usage.cache_read_input_tokens || 0,
3068
+ cache_creation_input_tokens:
3069
+ data.usage.cache_creation_input_tokens || 0,
2800
3070
  };
2801
3071
  }
2802
3072
  return normalized;
@@ -2865,6 +3135,9 @@ export async function chatWithTools(rawMessages, options) {
2865
3135
  options.onToken,
2866
3136
  signal,
2867
3137
  provider,
3138
+ options.onStall,
3139
+ options.streamStallMs,
3140
+ options.streamStallTimeoutMs,
2868
3141
  ),
2869
3142
  { signal, onRetry: options.onStreamRetry },
2870
3143
  );
@@ -2895,9 +3168,14 @@ export async function chatWithTools(rawMessages, options) {
2895
3168
  const choice = data.choices[0];
2896
3169
  const out = { message: choice.message };
2897
3170
  if (data.usage) {
3171
+ // OpenAI/DeepSeek/volcengine report cached prompt tokens AS PART of
3172
+ // prompt_tokens — split them out so cost prices the cached prefix at the
3173
+ // provider's cache rate, not full input. 0 (or absent) → input unchanged.
3174
+ const cached = _openaiCachedTokens(data.usage);
2898
3175
  out.usage = {
2899
- input_tokens: data.usage.prompt_tokens || 0,
3176
+ input_tokens: Math.max(0, (data.usage.prompt_tokens || 0) - cached),
2900
3177
  output_tokens: data.usage.completion_tokens || 0,
3178
+ cache_read_input_tokens: cached,
2901
3179
  };
2902
3180
  }
2903
3181
  return out;
@@ -3033,34 +3311,27 @@ export function formatProviderHttpError(provider, status) {
3033
3311
  * streamed (partial-output drops are preserved internally and never throw) — so
3034
3312
  * a retry can never duplicate visible text. Pure + exported for tests.
3035
3313
  */
3036
- export function _isRetryableStreamError(err, signal) {
3037
- if (isAbortError(err)) return false;
3038
- if (signal && signal.aborted) return false;
3039
- if (!err) return false;
3040
- const code = String(err.code || err.cause?.code || "").toUpperCase();
3041
- if (
3042
- [
3043
- "ECONNRESET",
3044
- "ETIMEDOUT",
3045
- "ECONNREFUSED",
3046
- "EAI_AGAIN",
3047
- "EPIPE",
3048
- "ENETUNREACH",
3049
- "ENOTFOUND",
3050
- "UND_ERR_SOCKET",
3051
- ].includes(code)
3052
- ) {
3053
- return true;
3054
- }
3055
- const msg = String(err.message || err).toLowerCase();
3056
- return /econnreset|etimedout|econnrefused|eai_again|socket hang ?up|terminated|fetch failed|network error|timed?\s*out|premature close/.test(
3057
- msg,
3058
- );
3059
- }
3060
-
3061
- /** Bounded auto-retry for streaming connection drops (Claude-Code 2.1.181). */
3062
- const STREAM_RETRY_MAX = 2;
3063
- const STREAM_RETRY_BASE_MS = 400;
3314
+ // Re-exported from the shared classifier (src/lib/stream-retry.js) so agent-core
3315
+ // internals and existing test imports keep resolving it from this module, while
3316
+ // the agent and chat streaming paths share ONE definition (no drift). The retry
3317
+ // budget constants (STREAM_RETRY_MAX / STREAM_RETRY_BASE_MS) come from there too.
3318
+ export const _isRetryableStreamError = isRetryableStreamError;
3319
+ // 2.1.185 parity: when a streaming response goes silent mid-flight (the TCP
3320
+ // connection is alive but no bytes arrive — a slow/overloaded API), surface a
3321
+ // "waiting for API response" hint after this many ms instead of leaving the
3322
+ // user staring at a frozen spinner. Upstream raised this from 10s to 20s.
3323
+ const STREAM_STALL_MS = 20000;
3324
+ // Hard inactivity timeout: a stream silent for this long is treated as a dead
3325
+ // connection — the watchdog cancels the reader and throws a RETRYABLE error so
3326
+ // `_retryStreamingChat` re-issues the request instead of hanging forever.
3327
+ // Default 180s mirrors the long-standing `cc chat`/`cc ask` stall guard
3328
+ // (chat-core.js STREAM_STALL_MS / CC_CHAT_STALL_MS) — generous enough that even
3329
+ // a slow local model's first token arrives in time, while still recovering from
3330
+ // a genuinely dead connection by default. Set llm.streamStallTimeoutMs (or pass
3331
+ // streamStallTimeoutMs: 0) to tune or disable. The 20s hint above still fires
3332
+ // first. Must exceed STREAM_STALL_MS.
3333
+ const STREAM_STALL_TIMEOUT_MS = 180000;
3334
+ const _STREAM_STALL = Symbol("stream-stall");
3064
3335
 
3065
3336
  /**
3066
3337
  * Run a streaming chat attempt with bounded auto-retry on transient API
@@ -3114,7 +3385,103 @@ function _finalizeTruncatedStream(finalize, state) {
3114
3385
  return out;
3115
3386
  }
3116
3387
 
3117
- async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
3388
+ // Wrap a ReadableStream reader so the read loop yields chunk values while a
3389
+ // watchdog (a) fires `onStall(elapsedMs, stallTimeoutMs)` at most once per silent
3390
+ // gap longer than `stallMs` (2.1.185 stream-stall hint; the 2nd arg lets the hint
3391
+ // surface the auto-retry deadline, 0 when no timeout) and (b) — when `stallTimeoutMs` is
3392
+ // set — cancels the reader and throws a RETRYABLE ETIMEDOUT after that long a
3393
+ // silence, so a permanently dead-but-open connection recovers via the retry
3394
+ // layer instead of hanging forever. The SAME in-flight read() promise is
3395
+ // re-raced against a fresh timer each tick, so no chunk is ever dropped or
3396
+ // double-read; `done`, errors, and aborts propagate unchanged. With neither a
3397
+ // hook nor a timeout this degrades to a plain read loop (zero extra timers).
3398
+ export async function* _iterateStreamWithStall(reader, opts = {}) {
3399
+ const onStall = typeof opts.onStall === "function" ? opts.onStall : null;
3400
+ const stallMs = opts.stallMs ?? STREAM_STALL_MS;
3401
+ const timeoutMs =
3402
+ opts.stallTimeoutMs == null ? STREAM_STALL_TIMEOUT_MS : opts.stallTimeoutMs;
3403
+ for (;;) {
3404
+ const readP = reader.read();
3405
+ // Fast path: nothing to watch for — await the chunk directly.
3406
+ if (!onStall && !timeoutMs) {
3407
+ const { done, value } = await readP;
3408
+ if (done) return;
3409
+ yield value;
3410
+ continue;
3411
+ }
3412
+ const start = Date.now();
3413
+ let notified = false;
3414
+ let result;
3415
+ for (;;) {
3416
+ const elapsed = Date.now() - start;
3417
+ // Hard inactivity timeout reached: cancel the dead stream (release the
3418
+ // socket) and throw a retryable error the dispatch seam re-issues.
3419
+ if (timeoutMs && elapsed >= timeoutMs) {
3420
+ try {
3421
+ await reader.cancel();
3422
+ } catch {
3423
+ /* best-effort release — the throw below is what matters */
3424
+ }
3425
+ const err = new Error(
3426
+ `stream stalled: no data from API for ${Math.round(elapsed / 1000)}s`,
3427
+ );
3428
+ err.code = "ETIMEDOUT";
3429
+ throw err;
3430
+ }
3431
+ // Wake at whichever fires first: the one-shot stall hint or the timeout.
3432
+ const waits = [];
3433
+ if (onStall && !notified) waits.push(stallMs - elapsed);
3434
+ if (timeoutMs) waits.push(timeoutMs - elapsed);
3435
+ const nextWait = waits.length ? Math.max(0, Math.min(...waits)) : null;
3436
+ if (nextWait == null) {
3437
+ // Hint already fired and no timeout — just wait for the chunk.
3438
+ result = await readP;
3439
+ break;
3440
+ }
3441
+ let timer;
3442
+ const wakeP = new Promise((resolve) => {
3443
+ timer = setTimeout(() => resolve(_STREAM_STALL), nextWait);
3444
+ if (timer && typeof timer.unref === "function") timer.unref();
3445
+ });
3446
+ let r;
3447
+ try {
3448
+ r = await Promise.race([readP, wakeP]);
3449
+ } finally {
3450
+ clearTimeout(timer);
3451
+ }
3452
+ if (r === _STREAM_STALL) {
3453
+ // A timer woke us. Fire the one-shot hint if we've crossed stallMs; the
3454
+ // loop re-evaluates `elapsed` to decide hint-vs-hard-timeout next tick.
3455
+ if (onStall && !notified && Date.now() - start >= stallMs) {
3456
+ notified = true;
3457
+ try {
3458
+ // 2.1.185: pass the hard-timeout deadline so the hint can tell the
3459
+ // user when the dead-but-open stream will be auto-retried (0 = no
3460
+ // timeout configured → caller shows no retry deadline).
3461
+ onStall(Date.now() - start, timeoutMs || 0);
3462
+ } catch {
3463
+ /* stall hint is best-effort — never break the stream over it */
3464
+ }
3465
+ }
3466
+ continue; // keep awaiting the SAME read() against a fresh timer
3467
+ }
3468
+ result = r;
3469
+ break;
3470
+ }
3471
+ if (result.done) return;
3472
+ yield result.value;
3473
+ }
3474
+ }
3475
+
3476
+ async function _chatOllamaStreaming(
3477
+ apiUrl,
3478
+ body,
3479
+ onToken,
3480
+ signal,
3481
+ onStall,
3482
+ stallMs,
3483
+ stallTimeoutMs,
3484
+ ) {
3118
3485
  const response = await fetch(apiUrl, {
3119
3486
  method: "POST",
3120
3487
  headers: { "Content-Type": "application/json" },
@@ -3129,9 +3496,11 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
3129
3496
  const decoder = new TextDecoder();
3130
3497
  let buf = "";
3131
3498
  try {
3132
- for (;;) {
3133
- const { done, value } = await reader.read();
3134
- if (done) break;
3499
+ for await (const value of _iterateStreamWithStall(reader, {
3500
+ onStall,
3501
+ stallMs,
3502
+ stallTimeoutMs,
3503
+ })) {
3135
3504
  buf += decoder.decode(value, { stream: true });
3136
3505
  let idx;
3137
3506
  while ((idx = buf.indexOf("\n")) >= 0) {
@@ -3157,7 +3526,14 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
3157
3526
  // the same shape chatWithTools returns non-streamed.
3158
3527
 
3159
3528
  function _anthropicInitState() {
3160
- return { text: "", blocks: {}, inputTokens: 0, outputTokens: 0 };
3529
+ return {
3530
+ text: "",
3531
+ blocks: {},
3532
+ inputTokens: 0,
3533
+ outputTokens: 0,
3534
+ cacheReadTokens: 0,
3535
+ cacheCreationTokens: 0,
3536
+ };
3161
3537
  }
3162
3538
 
3163
3539
  function _anthropicReduceLine(state, raw, onToken, onThinking) {
@@ -3172,8 +3548,13 @@ function _anthropicReduceLine(state, raw, onToken, onThinking) {
3172
3548
  return state;
3173
3549
  }
3174
3550
  if (obj.type === "message_start") {
3175
- state.inputTokens =
3176
- Number(obj.message?.usage?.input_tokens) || state.inputTokens;
3551
+ const u = obj.message?.usage || {};
3552
+ state.inputTokens = Number(u.input_tokens) || state.inputTokens;
3553
+ // Prompt-cache token counts ride message_start.usage (Anthropic caching).
3554
+ state.cacheReadTokens =
3555
+ Number(u.cache_read_input_tokens) || state.cacheReadTokens;
3556
+ state.cacheCreationTokens =
3557
+ Number(u.cache_creation_input_tokens) || state.cacheCreationTokens;
3177
3558
  } else if (obj.type === "content_block_start") {
3178
3559
  const cb = obj.content_block || {};
3179
3560
  state.blocks[obj.index] =
@@ -3252,10 +3633,17 @@ function _anthropicFinalize(state) {
3252
3633
  // tool turn — required by the API when extended thinking is on.
3253
3634
  if (thinkingBlocks.length) message._thinkingBlocks = thinkingBlocks;
3254
3635
  const out = { message };
3255
- if (state.inputTokens || state.outputTokens) {
3636
+ if (
3637
+ state.inputTokens ||
3638
+ state.outputTokens ||
3639
+ state.cacheReadTokens ||
3640
+ state.cacheCreationTokens
3641
+ ) {
3256
3642
  out.usage = {
3257
3643
  input_tokens: state.inputTokens,
3258
3644
  output_tokens: state.outputTokens,
3645
+ cache_read_input_tokens: state.cacheReadTokens,
3646
+ cache_creation_input_tokens: state.cacheCreationTokens,
3259
3647
  };
3260
3648
  }
3261
3649
  return out;
@@ -3276,6 +3664,9 @@ async function _chatAnthropicStreaming(
3276
3664
  onToken,
3277
3665
  signal,
3278
3666
  onThinking,
3667
+ onStall,
3668
+ stallMs,
3669
+ stallTimeoutMs,
3279
3670
  ) {
3280
3671
  const response = await fetch(apiUrl, {
3281
3672
  method: "POST",
@@ -3291,9 +3682,11 @@ async function _chatAnthropicStreaming(
3291
3682
  const decoder = new TextDecoder();
3292
3683
  let buf = "";
3293
3684
  try {
3294
- for (;;) {
3295
- const { done, value } = await reader.read();
3296
- if (done) break;
3685
+ for await (const value of _iterateStreamWithStall(reader, {
3686
+ onStall,
3687
+ stallMs,
3688
+ stallTimeoutMs,
3689
+ })) {
3297
3690
  buf += decoder.decode(value, { stream: true });
3298
3691
  const lines = buf.split("\n");
3299
3692
  buf = lines.pop() || "";
@@ -3316,8 +3709,30 @@ async function _chatAnthropicStreaming(
3316
3709
  // concatenated across chunks). usage rides the terminal chunk when
3317
3710
  // stream_options.include_usage was requested. Terminator: `data: [DONE]`.
3318
3711
 
3712
+ // OpenAI-compatible cached-prompt-token count. OpenAI / volcengine report it as
3713
+ // usage.prompt_tokens_details.cached_tokens; DeepSeek as
3714
+ // usage.prompt_cache_hit_tokens. In BOTH, prompt_tokens already INCLUDES the
3715
+ // cached count (unlike Anthropic, where input_tokens is the uncached remainder)
3716
+ // — so callers subtract it to recover the uncached input and avoid pricing the
3717
+ // cached prefix twice. Verified live against volcengine (field present, 0 when
3718
+ // the provider does not auto-cache).
3719
+ function _openaiCachedTokens(usage) {
3720
+ if (!usage || typeof usage !== "object") return 0;
3721
+ const detailed = Number(usage.prompt_tokens_details?.cached_tokens);
3722
+ if (Number.isFinite(detailed) && detailed > 0) return detailed;
3723
+ const deepseek = Number(usage.prompt_cache_hit_tokens);
3724
+ if (Number.isFinite(deepseek) && deepseek > 0) return deepseek;
3725
+ return 0;
3726
+ }
3727
+
3319
3728
  function _openaiInitState() {
3320
- return { text: "", tools: [], inputTokens: 0, outputTokens: 0 };
3729
+ return {
3730
+ text: "",
3731
+ tools: [],
3732
+ inputTokens: 0,
3733
+ outputTokens: 0,
3734
+ cacheReadTokens: 0,
3735
+ };
3321
3736
  }
3322
3737
 
3323
3738
  function _openaiReduceLine(state, raw, onToken) {
@@ -3354,7 +3769,11 @@ function _openaiReduceLine(state, raw, onToken) {
3354
3769
  }
3355
3770
  }
3356
3771
  if (obj.usage) {
3357
- state.inputTokens = Number(obj.usage.prompt_tokens) || state.inputTokens;
3772
+ const cached = _openaiCachedTokens(obj.usage);
3773
+ const prompt = Number(obj.usage.prompt_tokens);
3774
+ if (Number.isFinite(prompt))
3775
+ state.inputTokens = Math.max(0, prompt - cached);
3776
+ if (cached) state.cacheReadTokens = cached;
3358
3777
  state.outputTokens =
3359
3778
  Number(obj.usage.completion_tokens) || state.outputTokens;
3360
3779
  }
@@ -3370,10 +3789,11 @@ function _openaiFinalize(state) {
3370
3789
  const message = { role: "assistant", content: state.text };
3371
3790
  if (toolCalls.length) message.tool_calls = toolCalls;
3372
3791
  const out = { message };
3373
- if (state.inputTokens || state.outputTokens) {
3792
+ if (state.inputTokens || state.outputTokens || state.cacheReadTokens) {
3374
3793
  out.usage = {
3375
3794
  input_tokens: state.inputTokens,
3376
3795
  output_tokens: state.outputTokens,
3796
+ cache_read_input_tokens: state.cacheReadTokens || 0,
3377
3797
  };
3378
3798
  }
3379
3799
  return out;
@@ -3393,6 +3813,9 @@ async function _chatOpenAIStreaming(
3393
3813
  onToken,
3394
3814
  signal,
3395
3815
  provider,
3816
+ onStall,
3817
+ stallMs,
3818
+ stallTimeoutMs,
3396
3819
  ) {
3397
3820
  const response = await fetch(apiUrl, {
3398
3821
  method: "POST",
@@ -3411,9 +3834,11 @@ async function _chatOpenAIStreaming(
3411
3834
  const decoder = new TextDecoder();
3412
3835
  let buf = "";
3413
3836
  try {
3414
- for (;;) {
3415
- const { done, value } = await reader.read();
3416
- if (done) break;
3837
+ for await (const value of _iterateStreamWithStall(reader, {
3838
+ onStall,
3839
+ stallMs,
3840
+ stallTimeoutMs,
3841
+ })) {
3417
3842
  buf += decoder.decode(value, { stream: true });
3418
3843
  const lines = buf.split("\n");
3419
3844
  buf = lines.pop() || "";