chainlesschain 0.162.95 → 0.162.96

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 (217) 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-Bo9Ux24z.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-CExgXSQT.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-BNnZhRTZ.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-D9xfqFCT.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-Cpgu8uCw.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CHdEJnNE.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-Cbw-PxBq.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-ChHTZK_6.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-Dl6z6ZH7.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Cj8d9bru.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-C2YobW4j.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-CxfV5FGT.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-CiwOASMj.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CTg_2iKW.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-HMbsTmtw.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-DyNDqGR2.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-9-x5M7qy.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-4msgDaYJ.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID--2Beb5Po.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-T6nq0Lzs.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-D_Q6T10v.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-TnN21bJx.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-kky_uIij.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-C1GkNY4B.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-5-_Fc5YV.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-CmHiB7sr.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-Cuci_BPI.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-BU0cO2LD.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-BDFNgEcx.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-DITXUzHz.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Ea-07CRd.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-D6OBbRVG.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-eiKpIJMi.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-M-X-T8UF.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-xcZb9Ekv.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-DCfoTPRC.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-BWfGDzD8.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-imBuld-U.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C2kcsq7I.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-DObvXSXP.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-C-6OGJMe.js} +4 -4
  45. package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-ub7Ozxci.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-CR2W89Vk.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-DNA_Wty7.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-rT4Wb4rQ.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CK-hARKv.js} +2 -2
  50. package/src/assets/web-panel/assets/PdhVaultBrowser-C2pbtNbT.js +7 -0
  51. package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-B8EIEqGJ.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-BxNVNU3s.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-CT9pbkyY.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-DhCNd2CS.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-Qngnx-vQ.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Dfn-nQc1.js} +2 -2
  57. package/src/assets/web-panel/assets/Projects-BKdiSaPR.js +1 -0
  58. package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-4o9B_n-X.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-bs1qvF30.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-CuEnKA2K.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-DbfvmRb2.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-DvMJGAI3.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-oJ0BZvmA.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DnIVrY9O.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-CIWhI_Tc.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-DLrnpjwm.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CYmqgtVb.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-BUzCkoeu.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DYRE4bnS.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-_6MurdOg.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-UIdnF5Li.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-COx2CjT8.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-BcHAmkZo.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-CjYQS8yZ.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-BmfOPcWt.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-D5HuarNQ.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-C_fbProA.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-JPulpRx8.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-CnO-062O.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-HpNovvau.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-yMncxH7y.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-Cug-t7Np.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn--U1-NR-3.js} +2 -2
  84. package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-R-XvSJ3S.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CCI1i6Rp.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-C-mqoaLt.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-De_N2iRR.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-DbTM6sjZ.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-OcLPHSl5.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-D0Adylyc.js} +1 -1
  91. package/src/assets/web-panel/assets/index-7E_mHkDU.js +1 -0
  92. package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-AY937BxS.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B6hgy5VC.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-B7XnpLHk.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-B7gw-tDm.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-BBgDgkwW.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BMmeiw88.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BU6XtnSx.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-Bi7VUG0R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-BtE3cw9m.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-C9ej-MF0.js} +3 -3
  102. package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-CIEiQl1j.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CKgaW9E5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-CY0KKR5f.js} +1 -1
  105. package/src/assets/web-panel/assets/index-CbV3-WlB.js +1 -0
  106. package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-Co1UkcFX.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CzP5d5LW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-D0NJ5lRi.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-D0gBsqTX.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-D5F6shtn.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-DVtamtlm.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-DWT18hDG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DaXb4B9P.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-DjQA0Vez.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-Ds8NJVEL.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-DuP-8HWu.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-DwLTiJ31.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-T3VhH8cO.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-TDY1o0w3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DOYcemym.js → index-TRy2Gc3V.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-W1OMxCmI.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-ftFRZbuc.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C7sBLech.js → index-iGPGaORA.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-jhy5KQvg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-qUSZJcgw.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-yavsyHif.js → index-w3f6lqkD.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-xdMc71Kh.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-xhX5eg2Z.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-zkTiW6d6.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-CIHqouWk.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-D1iUKftk.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-YOpWjmjL.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-C_1357UY.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-CDb4dq6o.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-D-OGYyhd.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve-hS-j69A0.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-tnjuvphU.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-BERfMMcU.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-DKpl2K9E.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-C_sX4D3N.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-DBcxC1vK.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-CeoDf2XI.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-DC9Pmj_u.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-CZA58J7M.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/activitypub.js +19 -3
  147. package/src/commands/agent.js +25 -0
  148. package/src/commands/ask.js +13 -0
  149. package/src/commands/config.js +17 -1
  150. package/src/commands/cost.js +6 -1
  151. package/src/commands/dao.js +16 -3
  152. package/src/commands/dlp.js +7 -1
  153. package/src/commands/hub.js +13 -0
  154. package/src/commands/mtc.js +37 -1
  155. package/src/gateways/discord/discord-formatter.js +18 -1
  156. package/src/gateways/telegram/telegram-formatter.js +6 -3
  157. package/src/gateways/ws/ws-server.js +19 -1
  158. package/src/harness/plugin-manager.js +20 -0
  159. package/src/harness/worktree-isolator.js +17 -1
  160. package/src/lib/__tests__/model-deprecation.test.js +194 -0
  161. package/src/lib/a2a-protocol.js +23 -9
  162. package/src/lib/agent-core.js +2 -5
  163. package/src/lib/audit-mtc.js +40 -2
  164. package/src/lib/chat-core.js +141 -18
  165. package/src/lib/cli-anything-bridge.js +5 -0
  166. package/src/lib/cli-numeric.js +38 -0
  167. package/src/lib/config-manager.js +37 -6
  168. package/src/lib/cowork/ab-comparator-cli.js +6 -2
  169. package/src/lib/cowork-cron.js +45 -34
  170. package/src/lib/cowork-learning.js +31 -1
  171. package/src/lib/cowork-share.js +39 -3
  172. package/src/lib/cowork-template-marketplace.js +54 -2
  173. package/src/lib/cowork-workflow.js +33 -6
  174. package/src/lib/cross-chain-mtc.js +41 -12
  175. package/src/lib/crypto-manager.js +12 -2
  176. package/src/lib/dbevo.js +19 -8
  177. package/src/lib/downloader.js +24 -9
  178. package/src/lib/evomap-governance.js +4 -4
  179. package/src/lib/evomap-manager.js +54 -7
  180. package/src/lib/fatal-handler.js +48 -0
  181. package/src/lib/file-checkpoint.js +26 -0
  182. package/src/lib/git-integration.js +28 -0
  183. package/src/lib/goal-store.js +36 -10
  184. package/src/lib/hierarchical-memory.js +6 -0
  185. package/src/lib/llm-pricing.js +52 -2
  186. package/src/lib/mcp-oauth.js +37 -7
  187. package/src/lib/memory-manager.js +23 -2
  188. package/src/lib/model-deprecation.js +249 -0
  189. package/src/lib/pdh-feedback-ledger.js +131 -0
  190. package/src/lib/permanent-memory.js +30 -2
  191. package/src/lib/permission-rules.cjs +11 -3
  192. package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
  193. package/src/lib/personal-data-hub-wiring.js +36 -13
  194. package/src/lib/project-instructions.js +10 -4
  195. package/src/lib/runnable-provider.js +62 -31
  196. package/src/lib/session-usage.js +34 -1
  197. package/src/lib/skill-loader.js +4 -1
  198. package/src/lib/skill-packs/generator.js +25 -2
  199. package/src/lib/stream-retry.js +56 -0
  200. package/src/lib/sync-credentials.js +53 -17
  201. package/src/lib/token-tracker.js +12 -2
  202. package/src/lib/with-file-lock.js +87 -0
  203. package/src/repl/agent-repl.js +39 -7
  204. package/src/repl/chat-repl.js +10 -0
  205. package/src/repl/session-cost.js +31 -3
  206. package/src/runtime/agent-core.js +474 -53
  207. package/src/runtime/coding-agent-contract-shared.cjs +58 -1
  208. package/src/runtime/coding-agent-policy.cjs +10 -0
  209. package/src/runtime/file-ref-expander.js +29 -1
  210. package/src/runtime/headless-runner.js +12 -1
  211. package/src/runtime/headless-stream.js +131 -5
  212. package/src/skills/video-editing/tools/commit.js +15 -1
  213. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
  214. package/src/assets/web-panel/assets/Projects-CXydUXp8.js +0 -1
  215. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
  216. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
  217. 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,99 @@ 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)` at most once per silent gap longer
3390
+ // than `stallMs` (2.1.185 stream-stall hint) and (b) — when `stallTimeoutMs` is
3391
+ // set — cancels the reader and throws a RETRYABLE ETIMEDOUT after that long a
3392
+ // silence, so a permanently dead-but-open connection recovers via the retry
3393
+ // layer instead of hanging forever. The SAME in-flight read() promise is
3394
+ // re-raced against a fresh timer each tick, so no chunk is ever dropped or
3395
+ // double-read; `done`, errors, and aborts propagate unchanged. With neither a
3396
+ // hook nor a timeout this degrades to a plain read loop (zero extra timers).
3397
+ export async function* _iterateStreamWithStall(reader, opts = {}) {
3398
+ const onStall = typeof opts.onStall === "function" ? opts.onStall : null;
3399
+ const stallMs = opts.stallMs ?? STREAM_STALL_MS;
3400
+ const timeoutMs =
3401
+ opts.stallTimeoutMs == null ? STREAM_STALL_TIMEOUT_MS : opts.stallTimeoutMs;
3402
+ for (;;) {
3403
+ const readP = reader.read();
3404
+ // Fast path: nothing to watch for — await the chunk directly.
3405
+ if (!onStall && !timeoutMs) {
3406
+ const { done, value } = await readP;
3407
+ if (done) return;
3408
+ yield value;
3409
+ continue;
3410
+ }
3411
+ const start = Date.now();
3412
+ let notified = false;
3413
+ let result;
3414
+ for (;;) {
3415
+ const elapsed = Date.now() - start;
3416
+ // Hard inactivity timeout reached: cancel the dead stream (release the
3417
+ // socket) and throw a retryable error the dispatch seam re-issues.
3418
+ if (timeoutMs && elapsed >= timeoutMs) {
3419
+ try {
3420
+ await reader.cancel();
3421
+ } catch {
3422
+ /* best-effort release — the throw below is what matters */
3423
+ }
3424
+ const err = new Error(
3425
+ `stream stalled: no data from API for ${Math.round(elapsed / 1000)}s`,
3426
+ );
3427
+ err.code = "ETIMEDOUT";
3428
+ throw err;
3429
+ }
3430
+ // Wake at whichever fires first: the one-shot stall hint or the timeout.
3431
+ const waits = [];
3432
+ if (onStall && !notified) waits.push(stallMs - elapsed);
3433
+ if (timeoutMs) waits.push(timeoutMs - elapsed);
3434
+ const nextWait = waits.length ? Math.max(0, Math.min(...waits)) : null;
3435
+ if (nextWait == null) {
3436
+ // Hint already fired and no timeout — just wait for the chunk.
3437
+ result = await readP;
3438
+ break;
3439
+ }
3440
+ let timer;
3441
+ const wakeP = new Promise((resolve) => {
3442
+ timer = setTimeout(() => resolve(_STREAM_STALL), nextWait);
3443
+ if (timer && typeof timer.unref === "function") timer.unref();
3444
+ });
3445
+ let r;
3446
+ try {
3447
+ r = await Promise.race([readP, wakeP]);
3448
+ } finally {
3449
+ clearTimeout(timer);
3450
+ }
3451
+ if (r === _STREAM_STALL) {
3452
+ // A timer woke us. Fire the one-shot hint if we've crossed stallMs; the
3453
+ // loop re-evaluates `elapsed` to decide hint-vs-hard-timeout next tick.
3454
+ if (onStall && !notified && Date.now() - start >= stallMs) {
3455
+ notified = true;
3456
+ try {
3457
+ onStall(Date.now() - start);
3458
+ } catch {
3459
+ /* stall hint is best-effort — never break the stream over it */
3460
+ }
3461
+ }
3462
+ continue; // keep awaiting the SAME read() against a fresh timer
3463
+ }
3464
+ result = r;
3465
+ break;
3466
+ }
3467
+ if (result.done) return;
3468
+ yield result.value;
3469
+ }
3470
+ }
3471
+
3472
+ async function _chatOllamaStreaming(
3473
+ apiUrl,
3474
+ body,
3475
+ onToken,
3476
+ signal,
3477
+ onStall,
3478
+ stallMs,
3479
+ stallTimeoutMs,
3480
+ ) {
3118
3481
  const response = await fetch(apiUrl, {
3119
3482
  method: "POST",
3120
3483
  headers: { "Content-Type": "application/json" },
@@ -3129,9 +3492,11 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
3129
3492
  const decoder = new TextDecoder();
3130
3493
  let buf = "";
3131
3494
  try {
3132
- for (;;) {
3133
- const { done, value } = await reader.read();
3134
- if (done) break;
3495
+ for await (const value of _iterateStreamWithStall(reader, {
3496
+ onStall,
3497
+ stallMs,
3498
+ stallTimeoutMs,
3499
+ })) {
3135
3500
  buf += decoder.decode(value, { stream: true });
3136
3501
  let idx;
3137
3502
  while ((idx = buf.indexOf("\n")) >= 0) {
@@ -3157,7 +3522,14 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
3157
3522
  // the same shape chatWithTools returns non-streamed.
3158
3523
 
3159
3524
  function _anthropicInitState() {
3160
- return { text: "", blocks: {}, inputTokens: 0, outputTokens: 0 };
3525
+ return {
3526
+ text: "",
3527
+ blocks: {},
3528
+ inputTokens: 0,
3529
+ outputTokens: 0,
3530
+ cacheReadTokens: 0,
3531
+ cacheCreationTokens: 0,
3532
+ };
3161
3533
  }
3162
3534
 
3163
3535
  function _anthropicReduceLine(state, raw, onToken, onThinking) {
@@ -3172,8 +3544,13 @@ function _anthropicReduceLine(state, raw, onToken, onThinking) {
3172
3544
  return state;
3173
3545
  }
3174
3546
  if (obj.type === "message_start") {
3175
- state.inputTokens =
3176
- Number(obj.message?.usage?.input_tokens) || state.inputTokens;
3547
+ const u = obj.message?.usage || {};
3548
+ state.inputTokens = Number(u.input_tokens) || state.inputTokens;
3549
+ // Prompt-cache token counts ride message_start.usage (Anthropic caching).
3550
+ state.cacheReadTokens =
3551
+ Number(u.cache_read_input_tokens) || state.cacheReadTokens;
3552
+ state.cacheCreationTokens =
3553
+ Number(u.cache_creation_input_tokens) || state.cacheCreationTokens;
3177
3554
  } else if (obj.type === "content_block_start") {
3178
3555
  const cb = obj.content_block || {};
3179
3556
  state.blocks[obj.index] =
@@ -3252,10 +3629,17 @@ function _anthropicFinalize(state) {
3252
3629
  // tool turn — required by the API when extended thinking is on.
3253
3630
  if (thinkingBlocks.length) message._thinkingBlocks = thinkingBlocks;
3254
3631
  const out = { message };
3255
- if (state.inputTokens || state.outputTokens) {
3632
+ if (
3633
+ state.inputTokens ||
3634
+ state.outputTokens ||
3635
+ state.cacheReadTokens ||
3636
+ state.cacheCreationTokens
3637
+ ) {
3256
3638
  out.usage = {
3257
3639
  input_tokens: state.inputTokens,
3258
3640
  output_tokens: state.outputTokens,
3641
+ cache_read_input_tokens: state.cacheReadTokens,
3642
+ cache_creation_input_tokens: state.cacheCreationTokens,
3259
3643
  };
3260
3644
  }
3261
3645
  return out;
@@ -3276,6 +3660,9 @@ async function _chatAnthropicStreaming(
3276
3660
  onToken,
3277
3661
  signal,
3278
3662
  onThinking,
3663
+ onStall,
3664
+ stallMs,
3665
+ stallTimeoutMs,
3279
3666
  ) {
3280
3667
  const response = await fetch(apiUrl, {
3281
3668
  method: "POST",
@@ -3291,9 +3678,11 @@ async function _chatAnthropicStreaming(
3291
3678
  const decoder = new TextDecoder();
3292
3679
  let buf = "";
3293
3680
  try {
3294
- for (;;) {
3295
- const { done, value } = await reader.read();
3296
- if (done) break;
3681
+ for await (const value of _iterateStreamWithStall(reader, {
3682
+ onStall,
3683
+ stallMs,
3684
+ stallTimeoutMs,
3685
+ })) {
3297
3686
  buf += decoder.decode(value, { stream: true });
3298
3687
  const lines = buf.split("\n");
3299
3688
  buf = lines.pop() || "";
@@ -3316,8 +3705,30 @@ async function _chatAnthropicStreaming(
3316
3705
  // concatenated across chunks). usage rides the terminal chunk when
3317
3706
  // stream_options.include_usage was requested. Terminator: `data: [DONE]`.
3318
3707
 
3708
+ // OpenAI-compatible cached-prompt-token count. OpenAI / volcengine report it as
3709
+ // usage.prompt_tokens_details.cached_tokens; DeepSeek as
3710
+ // usage.prompt_cache_hit_tokens. In BOTH, prompt_tokens already INCLUDES the
3711
+ // cached count (unlike Anthropic, where input_tokens is the uncached remainder)
3712
+ // — so callers subtract it to recover the uncached input and avoid pricing the
3713
+ // cached prefix twice. Verified live against volcengine (field present, 0 when
3714
+ // the provider does not auto-cache).
3715
+ function _openaiCachedTokens(usage) {
3716
+ if (!usage || typeof usage !== "object") return 0;
3717
+ const detailed = Number(usage.prompt_tokens_details?.cached_tokens);
3718
+ if (Number.isFinite(detailed) && detailed > 0) return detailed;
3719
+ const deepseek = Number(usage.prompt_cache_hit_tokens);
3720
+ if (Number.isFinite(deepseek) && deepseek > 0) return deepseek;
3721
+ return 0;
3722
+ }
3723
+
3319
3724
  function _openaiInitState() {
3320
- return { text: "", tools: [], inputTokens: 0, outputTokens: 0 };
3725
+ return {
3726
+ text: "",
3727
+ tools: [],
3728
+ inputTokens: 0,
3729
+ outputTokens: 0,
3730
+ cacheReadTokens: 0,
3731
+ };
3321
3732
  }
3322
3733
 
3323
3734
  function _openaiReduceLine(state, raw, onToken) {
@@ -3354,7 +3765,11 @@ function _openaiReduceLine(state, raw, onToken) {
3354
3765
  }
3355
3766
  }
3356
3767
  if (obj.usage) {
3357
- state.inputTokens = Number(obj.usage.prompt_tokens) || state.inputTokens;
3768
+ const cached = _openaiCachedTokens(obj.usage);
3769
+ const prompt = Number(obj.usage.prompt_tokens);
3770
+ if (Number.isFinite(prompt))
3771
+ state.inputTokens = Math.max(0, prompt - cached);
3772
+ if (cached) state.cacheReadTokens = cached;
3358
3773
  state.outputTokens =
3359
3774
  Number(obj.usage.completion_tokens) || state.outputTokens;
3360
3775
  }
@@ -3370,10 +3785,11 @@ function _openaiFinalize(state) {
3370
3785
  const message = { role: "assistant", content: state.text };
3371
3786
  if (toolCalls.length) message.tool_calls = toolCalls;
3372
3787
  const out = { message };
3373
- if (state.inputTokens || state.outputTokens) {
3788
+ if (state.inputTokens || state.outputTokens || state.cacheReadTokens) {
3374
3789
  out.usage = {
3375
3790
  input_tokens: state.inputTokens,
3376
3791
  output_tokens: state.outputTokens,
3792
+ cache_read_input_tokens: state.cacheReadTokens || 0,
3377
3793
  };
3378
3794
  }
3379
3795
  return out;
@@ -3393,6 +3809,9 @@ async function _chatOpenAIStreaming(
3393
3809
  onToken,
3394
3810
  signal,
3395
3811
  provider,
3812
+ onStall,
3813
+ stallMs,
3814
+ stallTimeoutMs,
3396
3815
  ) {
3397
3816
  const response = await fetch(apiUrl, {
3398
3817
  method: "POST",
@@ -3411,9 +3830,11 @@ async function _chatOpenAIStreaming(
3411
3830
  const decoder = new TextDecoder();
3412
3831
  let buf = "";
3413
3832
  try {
3414
- for (;;) {
3415
- const { done, value } = await reader.read();
3416
- if (done) break;
3833
+ for await (const value of _iterateStreamWithStall(reader, {
3834
+ onStall,
3835
+ stallMs,
3836
+ stallTimeoutMs,
3837
+ })) {
3417
3838
  buf += decoder.decode(value, { stream: true });
3418
3839
  const lines = buf.split("\n");
3419
3840
  buf = lines.pop() || "";