chainlesschain 0.162.78 → 0.162.79

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 (196) hide show
  1. package/README.md +37 -1
  2. package/bin/chainlesschain.js +20 -1
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-BhKMd38k.js → AIOps-D89fD_7T.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BzhKY5C_.js → ActionButton-d75flwX8.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-CATIz2Jc.js → Analytics-gvvBEb4T.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-eCx64YWg.js → AppLayout-DzHOVS64.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-CGOHfCHj.js → Audit-15K7MhRC.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Dyr6R0Ra.js → Backup-BgVHsglI.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CVhBu7NZ.js → BaseInput-BRnvd4sF.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DOCCKp2k.js → Chat-B2vtXu7s.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DcXCCnjN.js → ChatBubbleRenderer-d1EFM3dh.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-3yjnENud.js → Checkbox-B5uV5Jhr.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-D06sn8kB.js → Codegen-cVDMBVGV.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Bjn5vFES.js → Col-DtL6q7Hw.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-BYHpQHmf.js → Community-DSyPZkqQ.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-MKRmnUDQ.js → Compact-CzfV_q4O.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CVtPe8dh.js → Compliance-B_xhhLE1.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BRu5M3dv.js → Cowork-Y49Qf0JX.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-D_7eU5Ut.js → Cron-B7JG-oLj.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BFyVp_e9.js → Crosschain-CukEkQtf.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DRoG7638.js → DID-Bd4UVKdn.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-NfM_v-Oe.js → Dashboard-DRCQl7H1.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CGN1Ksu0.js → Dropdown-De47CuRY.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DhsY_z_a.js → EmailListRenderer-C-wAkceB.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-vW_VsKUG.js → FamilyGuardDashboard-DhLF_E0k.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-D2ob_c7h.js → Federation-DAipUS1m.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cpem15Pr.js → FormItemContext-BCFyAd2j.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-P6XfmXwT.js → GenericCardRenderer-wMdxrEV9.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-CfSeec1R.js → Git-CcwP7HUN.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-4ipIpqNl.js → Governance-VF760JcB.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CiQY_P9Y.js → Inference-DnR-GIn2.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-B3Qem78R.js → KnowledgeGraph-j-ewVKWO.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-De2zWVy6.js → Logs-DgB7wSor.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-CTNu4c1A.js → Marketplace-fiKjzVWE.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-Xx25EA2f.js → McpTools-BjXSMQrd.js} +5 -5
  37. package/src/assets/web-panel/assets/{Memory-YzzCCrch.js → Memory-C0L5lnSS.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CJZY98f0.js → MobileBridge-C0Rgg1Su.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-Dw7yl4KN.js → MobileProjects-BPA50hQb.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-D7TzGJhH.js → Mtc-BUsHuAjB.js} +4 -4
  41. package/src/assets/web-panel/assets/{MtcAudit-THyGhf0h.js → MtcAudit-dlH8Q_7U.js} +6 -6
  42. package/src/assets/web-panel/assets/{Multisig-RVxuGPUR.js → Multisig-L3_9beuW.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-DK7TCzKF.js → NLProgramming-BG4sXy27.js} +1 -1
  44. package/src/assets/web-panel/assets/Notes-felgIvGS.js +7 -0
  45. package/src/assets/web-panel/assets/{NotificationSettings-BMivJy85.js → NotificationSettings-DHDT96AK.js} +1 -1
  46. package/src/assets/web-panel/assets/OrderTableRenderer-BYrkEfvR.js +1 -0
  47. package/src/assets/web-panel/assets/{Organization-zVRtW1n_.js → Organization-DdtOLkHG.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-C0YLldFH.js → Overflow-DLw5Pni7.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-Xxgzghqp.js → P2P-B-mZ5EXz.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-DH_LO13b.js → PdhVaultBrowser-DeOmNCwK.js} +5 -5
  51. package/src/assets/web-panel/assets/{Permissions-CvAd1VBw.js → Permissions-BeQMX71K.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-CWQGgCAK.js → PersonalDataHub-D5PoqtQI.js} +3 -3
  53. package/src/assets/web-panel/assets/{Pipeline-BNAoh-Lb.js → Pipeline-B0f5sqKF.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-CNO5pFq-.js → Privacy-Dj_gcxio.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-WaVVDsm3.js → ProjectInit-DmfPgied.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D1WfkuJ3.js → ProjectSettings-BmBfocrv.js} +2 -2
  57. package/src/assets/web-panel/assets/Projects-BUif48cc.js +1 -0
  58. package/src/assets/web-panel/assets/{Providers-IOOJ4_wy.js → Providers-B0ztEAOV.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-ChHZqVZy.js → QuickAsk-BtMG4jH5.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-CSiW6Qv9.js → Recommend-DQKN1En8.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-BQe0rkfF.js → Reputation-D34CvUxg.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-Dem0Wxxb.js → Row-nnuDNx31.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-pBY5G41C.js → RssFeed-DHx9x8_B.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CtRepO6B.js → Search-CH-JYtn_.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-nrSlKpWq.js → Security-nNKBmzm4.js} +4 -4
  66. package/src/assets/web-panel/assets/{Services-DeaDBASi.js → Services-xYk_lDxy.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-Cz9R-Wjb.js → Skeleton-DTYyRduA.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-B3U-XLH3.js → Skills-DS19-9sF.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-Bu46dIA_.js → Sla-Cr_ir42Y.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-C9Z0V0pk.js → SpeechSettings-Ch5Iq7Wy.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-Ctj9KHHr.js → SyncSettings-CTblfa_E.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-D4upQgR_.js → Tasks-z7lz3hjG.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-JHsPGU_c.js → Templates-DGoMFd4r.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-uoaQL3fB.js → Tenant-DQUPvHxx.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CWRWr8bq.js → Terminal-Wt2wrbE6.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-BTicmSAV.js → TimelineRenderer-DGTc2UqL.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bp3BUe2K.js → Tokens-DPVnuARF.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-CgoISw5d.js → Trigger-DV5MPXC1.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-CC29awNT.js → Trust-BBkMKqwS.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-CB1SB6Nc.js → UkeySign-B_2E0qev.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-D7vptDUg.js → VideoEditing-CA-qKeVb.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-BWfjzF7p.js → Wallet-Ds4WURh-.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-Dzz5OnPc.js → WebAuthn-BApiHjzz.js} +5 -5
  84. package/src/assets/web-panel/assets/{WorkflowEditor-CiDeVmsG.js → WorkflowEditor-DFzmAnzV.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-DQbciNb5.js → chat-DZ6sHPit.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-DcLbPJzb.js → colors-DgbwhHtE.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-CvYrR3rc.js → compact-item-C4QVYSzd.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-BR4P7Rgm.js → createContext-yXIs4TwU.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-C-HfIeF7.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-IQ88RNRJ.js → hasIn-CPtuUtBl.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-B5W1vQHV.js → index-4g6pGxnX.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-DHIp5msb.js → index-B91hax9S.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BUTN1VlO.js → index-B9DUiQ24.js} +3 -3
  94. package/src/assets/web-panel/assets/index-BBgWatYO.js +1 -0
  95. package/src/assets/web-panel/assets/{index-DgaCUxpi.js → index-BCb7MYMS.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BmPuR0aA.js → index-BIAxMDe9.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dpmnk2qv.js → index-BJiGrT6V.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-D8OJdOc_.js → index-BOKVSmKh.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Mn8_ryOe.js → index-BWJGry74.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BiMlLIZ-.js → index-BaVYCMJa.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CPOupQSX.js → index-Bd0JznCS.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Bo7HAK6G.js → index-BpBCKK6W.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Db5LFFCN.js → index-BsXF9cn5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BQXs-5db.js → index-BxclR5gc.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BVb6RI7f.js → index-C0LyE6R6.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CiOZ_Whh.js → index-CBAgy5pf.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DdQBxvpt.js → index-CJ550XXg.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Cm74AosZ.js → index-CTlz3MP6.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bw0Dm_P6.js → index-CaC4gaQZ.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BxY0ozve.js → index-CnP2ftM5.js} +1 -1
  111. package/src/assets/web-panel/assets/index-CvCTol_u.js +1 -0
  112. package/src/assets/web-panel/assets/{index-eKd1n8pw.js → index-D2KRfjEI.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-b6FjzfoJ.js → index-DA0ePxNn.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CE2mqX8w.js → index-DC5iP1VB.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Bl1TSbTE.js → index-DCGmeXfl.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BH2RT15D.js → index-DHnWI0jj.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-UbB2IcFR.js → index-DN4qpkKQ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CgP5aQmA.js → index-DTLaRwKx.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BHxJnExB.js → index-DdFjcgqH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-JqOP7puJ.js → index-DopuoTzG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Dox9vEhP.js → index-DpDAGvyU.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DGwa8mnJ.js → index-EGVlUtH-.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-bRT7u-51.js → index-IFh9qCi0.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-Bu8931Yi.js → index-UEeNpaDD.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CCO8yc1h.js → index-ZCyyy3Zt.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CzERBV9P.js → index-aKZjRwjv.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CeCWyiFl.js → index-jBlY6-bF.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BhkZZXtI.js → index-kbcTc6Pf.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BvQpTO67.js → index-x9bXmSNB.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-C0arzCLE.js → initDefaultProps-BDsxioXk.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-C1K6JxwD.js → motion-erZ2fiIQ.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-DREsRLHj.js → move-BFiiw4WK.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-BtPS3EDq.js → omit-BRMVd4pM.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-BPz6tHoT.js → pickAttrs-CGjoFXsq.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-B0CR_CSI.js → placementArrow-DU_Bnf7w.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-Ch2ojiNn.js → responsiveObserve-DPdkYupg.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-9qU9vOhj.js → slide-JfHiG96y.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-Cr4fICjV.js → statusUtils-Dy-1guyd.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-Cor2-FwV.js → styleChecker-Chaljnux.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-BINo_rNH.js → useFlexGapSupport-DOHIHBgY.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-Dm1tDNYC.js → useFs-BxSoQhIY.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-__JgBEkX.js → usePersonalDataHub-MOR76PYB.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-1hQKpRgP.js → vnode-BBGIyeYD.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-C1EY9X2J.js → zoom-C1oFENec.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/audit.js +4 -3
  147. package/src/commands/automation.js +6 -14
  148. package/src/commands/bi.js +10 -9
  149. package/src/commands/codegen.js +5 -13
  150. package/src/commands/dao.js +8 -6
  151. package/src/commands/dbevo.js +13 -14
  152. package/src/commands/economy.js +3 -2
  153. package/src/commands/evolution.js +3 -2
  154. package/src/commands/federation.js +4 -3
  155. package/src/commands/governance.js +9 -4
  156. package/src/commands/hardening.js +5 -4
  157. package/src/commands/incentive.js +6 -5
  158. package/src/commands/kg.js +17 -10
  159. package/src/commands/lowcode.js +23 -11
  160. package/src/commands/marketplace.js +4 -3
  161. package/src/commands/mcp.js +17 -5
  162. package/src/commands/ops.js +9 -4
  163. package/src/commands/recommend.js +7 -5
  164. package/src/commands/scim.js +3 -2
  165. package/src/commands/session.js +9 -6
  166. package/src/commands/social.js +4 -3
  167. package/src/commands/sync.js +3 -2
  168. package/src/commands/tenant.js +11 -6
  169. package/src/commands/zkp.js +8 -9
  170. package/src/gateways/ws/ws-agent-handler.js +12 -3
  171. package/src/gateways/ws/ws-server.js +6 -0
  172. package/src/harness/background-task-manager.js +44 -18
  173. package/src/harness/mcp-client.js +125 -46
  174. package/src/lib/chat-core.js +209 -107
  175. package/src/lib/claude-code-bridge.js +13 -1
  176. package/src/lib/dao-governance.js +3 -3
  177. package/src/lib/downloader.js +82 -25
  178. package/src/lib/headless-config-command.js +62 -0
  179. package/src/lib/json-schema-output.js +55 -11
  180. package/src/lib/mcp-oauth.js +110 -21
  181. package/src/lib/mcp-serve.js +70 -11
  182. package/src/lib/multisig-runtime.js +22 -3
  183. package/src/lib/parse-json-option.js +35 -0
  184. package/src/lib/parse-number-option.js +27 -0
  185. package/src/lib/runnable-provider.js +97 -0
  186. package/src/repl/agent-repl.js +76 -17
  187. package/src/repl/config-summary.js +66 -0
  188. package/src/runtime/agent-core.js +189 -36
  189. package/src/runtime/headless-runner.js +49 -1
  190. package/src/runtime/headless-stream.js +34 -0
  191. package/src/assets/web-panel/assets/Notes-BaOU0psj.js +0 -7
  192. package/src/assets/web-panel/assets/OrderTableRenderer-BZOiY8Yw.js +0 -1
  193. package/src/assets/web-panel/assets/Projects-BzjvJYMW.js +0 -1
  194. package/src/assets/web-panel/assets/devWarning-CnV02N63.js +0 -1
  195. package/src/assets/web-panel/assets/index-DJ2gkaIH.js +0 -1
  196. package/src/assets/web-panel/assets/index-Dvm_-AOi.js +0 -1
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Runnability guards for LLM model/provider selection.
3
+ *
4
+ * The task-based auto model-selector (lib/task-model-selector.js) switches the
5
+ * model by detected task type — e.g. a "quick" message on the `anthropic`
6
+ * provider becomes `claude-haiku`. That is WRONG when the target provider has
7
+ * no usable API key: it silently routes you onto a model you can't call and you
8
+ * get a 401. These helpers make selection "runnable-first": never switch onto a
9
+ * provider that has no usable (present) key, and let callers detect that the
10
+ * configured provider itself is not runnable.
11
+ *
12
+ * "Usable key" = a keyless local provider (ollama), OR the active session's
13
+ * configured key, OR the provider's standard env var (`<PROVIDER>_API_KEY`).
14
+ * Empty/whitespace keys never count (a missing or cleared key is not usable).
15
+ */
16
+ import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
17
+
18
+ function nonEmpty(v) {
19
+ return typeof v === "string" && v.trim().length > 0;
20
+ }
21
+
22
+ /**
23
+ * Can we actually call `provider` right now?
24
+ *
25
+ * @param {string} provider
26
+ * @param {object} [opts]
27
+ * @param {string} [opts.apiKey] the session's configured key — only counts
28
+ * when `isActive` (it belongs to the ACTIVE
29
+ * provider, not an arbitrary one).
30
+ * @param {boolean} [opts.isActive=true]
31
+ * @param {object} [opts.env=process.env]
32
+ * @returns {boolean}
33
+ */
34
+ export function hasUsableKey(
35
+ provider,
36
+ { apiKey, isActive = true, env = process.env } = {},
37
+ ) {
38
+ const def = BUILT_IN_PROVIDERS[provider];
39
+ if (!def) return false;
40
+ if (!def.apiKeyEnv) return true; // keyless local provider (e.g. ollama)
41
+ if (isActive && nonEmpty(apiKey)) return true;
42
+ return nonEmpty(env[def.apiKeyEnv]);
43
+ }
44
+
45
+ /**
46
+ * Gate the task-based auto model-switch on runnability. Returns the recommended
47
+ * model ONLY when the (active) provider is runnable; otherwise returns null so
48
+ * the caller keeps the user's configured model instead of switching onto
49
+ * something it can't call.
50
+ *
51
+ * @param {object} args
52
+ * @param {string} args.provider
53
+ * @param {string} [args.currentModel]
54
+ * @param {string|null} [args.recommended] from selectModelForTask()
55
+ * @param {string} [args.apiKey]
56
+ * @param {object} [args.env=process.env]
57
+ * @returns {string|null} a model to switch to, or null to keep the current one
58
+ */
59
+ export function runnableTaskModel({
60
+ provider,
61
+ currentModel,
62
+ recommended,
63
+ apiKey,
64
+ env = process.env,
65
+ } = {}) {
66
+ if (!recommended || recommended === currentModel) return null;
67
+ return hasUsableKey(provider, { apiKey, isActive: true, env })
68
+ ? recommended
69
+ : null;
70
+ }
71
+
72
+ /**
73
+ * Find a provider we can actually run, "runnable-first": keep `provider` when
74
+ * it has a usable key; otherwise fall back to the first built-in provider whose
75
+ * env key is set; otherwise the keyless local provider (ollama). Returns
76
+ * `{ provider, runnable, fellBackFrom?, keyless? }`. Pure given `env`.
77
+ */
78
+ export function pickRunnableProvider({
79
+ provider,
80
+ apiKey,
81
+ env = process.env,
82
+ } = {}) {
83
+ if (provider && hasUsableKey(provider, { apiKey, isActive: true, env })) {
84
+ return { provider, runnable: true };
85
+ }
86
+ for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {
87
+ if (def.apiKeyEnv && nonEmpty(env[def.apiKeyEnv])) {
88
+ return { provider: name, runnable: true, fellBackFrom: provider || null };
89
+ }
90
+ }
91
+ return {
92
+ provider: "ollama",
93
+ runnable: true,
94
+ keyless: true,
95
+ fellBackFrom: provider || null,
96
+ };
97
+ }
@@ -52,6 +52,7 @@ import {
52
52
  detectTaskType,
53
53
  selectModelForTask,
54
54
  } from "../lib/task-model-selector.js";
55
+ import { runnableTaskModel, hasUsableKey } from "../lib/runnable-provider.js";
55
56
  import { CLIPermanentMemory } from "../lib/permanent-memory.js";
56
57
  import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
57
58
  import {
@@ -743,7 +744,9 @@ export async function startAgentRepl(options = {}) {
743
744
  _bundleResolved.approvalPolicy.default,
744
745
  );
745
746
  // Mirror it so Shift+Tab cycling starts from the real tier.
746
- const applied = parsePermissionTier(_bundleResolved.approvalPolicy.default);
747
+ const applied = parsePermissionTier(
748
+ _bundleResolved.approvalPolicy.default,
749
+ );
747
750
  if (applied) _sessionTier = applied;
748
751
  } catch (_err) {
749
752
  // Non-critical — invalid policy value is silently ignored
@@ -1307,7 +1310,7 @@ export async function startAgentRepl(options = {}) {
1307
1310
  ` ${chalk.cyan("/theme")} Color theme (/theme <auto|dark|light|mono>; mono = no color)`,
1308
1311
  );
1309
1312
  logger.log(
1310
- ` ${chalk.cyan("/config")} Effective config (provider/model, keys masked)`,
1313
+ ` ${chalk.cyan("/config")} Show config; ${chalk.cyan("/config <key>")} read, ${chalk.cyan("/config <key>=<val>")} set`,
1311
1314
  );
1312
1315
  logger.log(
1313
1316
  ` ${chalk.cyan("/doctor")} Session health check (provider/key/IDE/MCP/hooks)`,
@@ -2695,18 +2698,43 @@ export async function startAgentRepl(options = {}) {
2695
2698
 
2696
2699
  // `/config` — effective configuration (secret-safe): the LLM provider/model
2697
2700
  // in effect, whether keys are set, web-search backend, config path.
2698
- if (trimmed === "/config" || trimmed === "/config ") {
2701
+ // `/config <key>` reads a value; `/config <key>=<value>` (Claude-Code
2702
+ // parity) or `/config <key> <value>` persists one. Secrets stay masked.
2703
+ if (trimmed === "/config" || trimmed.startsWith("/config ")) {
2699
2704
  try {
2700
- const { loadConfig } = await import("../lib/config-manager.js");
2705
+ const cm = await import("../lib/config-manager.js");
2701
2706
  const { getConfigPath } = await import("../lib/paths.js");
2702
- const { renderConfigSummary } = await import("./config-summary.js");
2703
- logger.log(
2704
- renderConfigSummary(loadConfig(), {
2705
- path: getConfigPath(),
2706
- activeProvider: provider,
2707
- activeModel: _curModel || model,
2708
- }),
2709
- );
2707
+ const {
2708
+ renderConfigSummary,
2709
+ parseConfigCommand,
2710
+ renderConfigGet,
2711
+ renderConfigSet,
2712
+ } = await import("./config-summary.js");
2713
+ const cmd = parseConfigCommand(trimmed.slice("/config".length));
2714
+ if (cmd.action === "error") {
2715
+ logger.error(chalk.red(`/config: ${cmd.message}`));
2716
+ } else if (cmd.action === "get") {
2717
+ logger.log(renderConfigGet(cmd.key, cm.getConfigValue(cmd.key)));
2718
+ } else if (cmd.action === "set") {
2719
+ cm.setConfigValue(cmd.key, cmd.value);
2720
+ logger.log(
2721
+ chalk.green("✓ ") +
2722
+ renderConfigSet(cmd.key, cm.getConfigValue(cmd.key)),
2723
+ );
2724
+ logger.log(
2725
+ chalk.gray(
2726
+ " (persisted; provider/model changes apply to new sessions)",
2727
+ ),
2728
+ );
2729
+ } else {
2730
+ logger.log(
2731
+ renderConfigSummary(cm.loadConfig(), {
2732
+ path: getConfigPath(),
2733
+ activeProvider: provider,
2734
+ activeModel: _curModel || model,
2735
+ }),
2736
+ );
2737
+ }
2710
2738
  } catch (err) {
2711
2739
  logger.error(chalk.red(`/config failed: ${err.message}`));
2712
2740
  }
@@ -3080,16 +3108,35 @@ export async function startAgentRepl(options = {}) {
3080
3108
  // Slot-filling failure is non-critical
3081
3109
  }
3082
3110
 
3083
- // Auto-select best model based on task type
3111
+ // Auto-select best model based on task type — but ONLY onto a runnable
3112
+ // provider. The selector maps e.g. "fast" → claude-haiku on anthropic; if
3113
+ // there's no usable key for the provider, never switch there (you'd just
3114
+ // get a 401). Runnable-first: keep the configured (working) model instead.
3084
3115
  let activeModel = model;
3085
3116
  const taskDetection = detectTaskType(promptText);
3086
3117
  if (taskDetection.confidence > 0.3) {
3087
3118
  const recommended = selectModelForTask(provider, taskDetection.taskType);
3088
- if (recommended && recommended !== activeModel) {
3089
- activeModel = recommended;
3119
+ const switchTo = runnableTaskModel({
3120
+ provider,
3121
+ currentModel: activeModel,
3122
+ recommended,
3123
+ apiKey,
3124
+ });
3125
+ if (switchTo) {
3126
+ activeModel = switchTo;
3090
3127
  logger.info(
3091
3128
  chalk.gray(`[auto] ${taskDetection.name} → ${activeModel}`),
3092
3129
  );
3130
+ } else if (
3131
+ recommended &&
3132
+ recommended !== activeModel &&
3133
+ !hasUsableKey(provider, { apiKey })
3134
+ ) {
3135
+ logger.info(
3136
+ chalk.gray(
3137
+ `[auto] ${taskDetection.name}: keeping ${activeModel} — no usable key for "${provider}" (skipping ${recommended})`,
3138
+ ),
3139
+ );
3093
3140
  }
3094
3141
  }
3095
3142
 
@@ -3122,7 +3169,8 @@ export async function startAgentRepl(options = {}) {
3122
3169
  ? {
3123
3170
  onToken: (t) => {
3124
3171
  // Separate the answer from the dimmed reasoning above it (once).
3125
- if (!_liveStreamed && _liveThinkStarted) process.stdout.write("\n");
3172
+ if (!_liveStreamed && _liveThinkStarted)
3173
+ process.stdout.write("\n");
3126
3174
  _liveStreamed = true;
3127
3175
  process.stdout.write(t);
3128
3176
  },
@@ -3143,6 +3191,16 @@ export async function startAgentRepl(options = {}) {
3143
3191
  thinking: reasoning,
3144
3192
  } = await agentLoop(messages, {
3145
3193
  ...liveOpts,
3194
+ // Visible auto-retry feedback (Claude-Code 2.1.181): when the model's
3195
+ // streaming call hits a transient connection drop and retries, tell the
3196
+ // user instead of leaving them staring at a silent pause. To stderr so
3197
+ // it never corrupts the streamed answer on stdout.
3198
+ onStreamRetry: (attempt) =>
3199
+ process.stderr.write(
3200
+ chalk.dim(
3201
+ ` ⟳ connection dropped — retrying (attempt ${attempt})…\n`,
3202
+ ),
3203
+ ),
3146
3204
  signal: _turnAbort.signal,
3147
3205
  provider,
3148
3206
  model: activeModel,
@@ -3242,7 +3300,8 @@ export async function startAgentRepl(options = {}) {
3242
3300
  } else {
3243
3301
  // Phase G #2 — route through StreamRouter so REPL / WS / future
3244
3302
  // streaming providers share one StreamEvent protocol.
3245
- const { streamAgentResponse } = await import("../lib/agent-stream.js");
3303
+ const { streamAgentResponse } =
3304
+ await import("../lib/agent-stream.js");
3246
3305
  process.stdout.write("\n");
3247
3306
  const noStream = options.noStream === true;
3248
3307
  const streamResult = await streamAgentResponse(effectiveResponse, {
@@ -17,6 +17,72 @@ export function maskSecret(v) {
17
17
  return s.length <= 4 ? "set (hidden)" : `set (…${s.slice(-4)})`;
18
18
  }
19
19
 
20
+ /**
21
+ * A config key whose value must never be echoed in plaintext in the
22
+ * (shoulder-surfable) interactive REPL — apiKey / secret / token / password.
23
+ * Matched on the last dotted segment (e.g. `llm.apiKey`, `webSearch.apiKey`).
24
+ */
25
+ const SECRET_KEY_RE = /(api[_-]?key|secret|token|password|passwd)$/i;
26
+ export function isSecretConfigKey(key) {
27
+ if (!key) return false;
28
+ const last = String(key).split(".").pop();
29
+ return SECRET_KEY_RE.test(last);
30
+ }
31
+
32
+ /**
33
+ * Parse the argument string after `/config` into an action.
34
+ * Mirrors Claude Code's `/config key=value` syntax, and also accepts the
35
+ * `/config key value` form. Returns one of:
36
+ * { action: "show" } // `/config`
37
+ * { action: "get", key } // `/config llm.model`
38
+ * { action: "set", key, value } // `/config llm.model=opus` | `/config llm.model opus`
39
+ * { action: "error", message }
40
+ * `value` is the raw string; the caller coerces (true/false/null/number) via
41
+ * config-manager's setConfigValue.
42
+ *
43
+ * @param {string} argStr everything after the literal `/config`
44
+ */
45
+ export function parseConfigCommand(argStr) {
46
+ const s = (argStr || "").trim();
47
+ if (s === "") return { action: "show" };
48
+
49
+ const eq = s.indexOf("=");
50
+ if (eq !== -1) {
51
+ const key = s.slice(0, eq).trim();
52
+ const value = s.slice(eq + 1).trim();
53
+ if (!key) return { action: "error", message: "missing key before '='" };
54
+ return { action: "set", key, value };
55
+ }
56
+
57
+ // `key value` — split on the first run of whitespace.
58
+ const m = s.match(/^(\S+)\s+([\s\S]+)$/);
59
+ if (m) return { action: "set", key: m[1], value: m[2].trim() };
60
+
61
+ // Bare token → read that key.
62
+ return { action: "get", key: s };
63
+ }
64
+
65
+ /** Render a single config value for `/config <key>`, masking secrets. */
66
+ export function renderConfigGet(key, value) {
67
+ if (value === undefined) return `${key} = (unset)`;
68
+ if (isSecretConfigKey(key)) return `${key} = ${maskSecret(value)}`;
69
+ const shown =
70
+ value !== null && typeof value === "object"
71
+ ? JSON.stringify(value)
72
+ : String(value);
73
+ return `${key} = ${shown}`;
74
+ }
75
+
76
+ /** Render the confirmation line after `/config <key>=<value>`. */
77
+ export function renderConfigSet(key, storedValue) {
78
+ if (isSecretConfigKey(key)) return `set ${key} = ${maskSecret(storedValue)}`;
79
+ const shown =
80
+ storedValue !== null && typeof storedValue === "object"
81
+ ? JSON.stringify(storedValue)
82
+ : String(storedValue);
83
+ return `set ${key} = ${shown}`;
84
+ }
85
+
20
86
  /**
21
87
  * @param {object|null} config loaded config.json
22
88
  * @param {object} [opts] { path, activeProvider, activeModel }
@@ -1207,6 +1207,37 @@ export async function executeTool(name, args, context = {}) {
1207
1207
  return toolResult;
1208
1208
  }
1209
1209
 
1210
+ /**
1211
+ * Write a file then verify the on-disk byte count matches the intended
1212
+ * content. Network drives and cloud-synced folders (OneDrive / Dropbox /
1213
+ * Google Drive) can silently truncate a write or leave a 0-byte file; without
1214
+ * this check the agent reports `success` on a corrupted write and moves on.
1215
+ * Parity with Claude-Code 2.1.181 ("Fixed Write/Edit producing 0-byte or
1216
+ * truncated files on network drives and cloud-synced folders").
1217
+ *
1218
+ * Returns `{ size }` (actual on-disk bytes) on success, or `{ error }`
1219
+ * describing the truncation so the caller surfaces it as a tool error instead
1220
+ * of a false success. `fsImpl` is injectable for unit tests.
1221
+ */
1222
+ export function writeFileVerified(filePath, content, fsImpl = fs) {
1223
+ const expected = Buffer.byteLength(content, "utf8");
1224
+ fsImpl.writeFileSync(filePath, content, "utf8");
1225
+ let actual;
1226
+ try {
1227
+ actual = fsImpl.statSync(filePath).size;
1228
+ } catch (err) {
1229
+ return {
1230
+ error: `Write verification failed: cannot stat ${filePath} after writing (${err.message}). The file may be on an unreliable network or cloud-synced drive.`,
1231
+ };
1232
+ }
1233
+ if (actual !== expected) {
1234
+ return {
1235
+ error: `Write truncated: expected ${expected} bytes but only ${actual} reached disk for ${filePath}. A network drive or cloud-sync folder (OneDrive/Dropbox/Google Drive) may have interrupted the write — retry, or write to a local path.`,
1236
+ };
1237
+ }
1238
+ return { size: actual };
1239
+ }
1240
+
1210
1241
  /**
1211
1242
  * Inner tool execution — no hooks, no plan-mode checks.
1212
1243
  */
@@ -1297,11 +1328,12 @@ async function executeToolInner(
1297
1328
  if (!fs.existsSync(dir)) {
1298
1329
  fs.mkdirSync(dir, { recursive: true });
1299
1330
  }
1300
- fs.writeFileSync(filePath, args.content, "utf8");
1331
+ const wrote = writeFileVerified(filePath, args.content);
1332
+ if (wrote.error) return attachDescriptor({ error: wrote.error });
1301
1333
  return attachDescriptor({
1302
1334
  success: true,
1303
1335
  path: filePath,
1304
- size: args.content.length,
1336
+ size: wrote.size,
1305
1337
  });
1306
1338
  }
1307
1339
 
@@ -1315,8 +1347,13 @@ async function executeToolInner(
1315
1347
  return attachDescriptor({ error: "old_string not found in file" });
1316
1348
  }
1317
1349
  const newContent = content.replace(args.old_string, args.new_string);
1318
- fs.writeFileSync(filePath, newContent, "utf8");
1319
- return attachDescriptor({ success: true, path: filePath });
1350
+ const wrote = writeFileVerified(filePath, newContent);
1351
+ if (wrote.error) return attachDescriptor({ error: wrote.error });
1352
+ return attachDescriptor({
1353
+ success: true,
1354
+ path: filePath,
1355
+ size: wrote.size,
1356
+ });
1320
1357
  }
1321
1358
 
1322
1359
  case "edit_file_hashed": {
@@ -1358,10 +1395,12 @@ async function executeToolInner(
1358
1395
  ...(snippet && { current_snippet: snippet }),
1359
1396
  });
1360
1397
  }
1361
- fs.writeFileSync(filePath, result.content, "utf8");
1398
+ const wrote = writeFileVerified(filePath, result.content);
1399
+ if (wrote.error) return attachDescriptor({ error: wrote.error });
1362
1400
  return attachDescriptor({
1363
1401
  success: true,
1364
1402
  path: filePath,
1403
+ size: wrote.size,
1365
1404
  lineNumber: result.lineNumber,
1366
1405
  previousContent: result.previousContent,
1367
1406
  });
@@ -2566,11 +2605,15 @@ export async function chatWithTools(rawMessages, options) {
2566
2605
  // same {message, usage} shape is returned, so the agent loop is unchanged.
2567
2606
  // Without onToken we keep the cheaper single-shot non-streaming request.
2568
2607
  if (typeof options.onToken === "function") {
2569
- return await _chatOllamaStreaming(
2570
- apiUrl,
2571
- { model, messages: ollamaMessages, tools },
2572
- options.onToken,
2573
- signal,
2608
+ return await _retryStreamingChat(
2609
+ () =>
2610
+ _chatOllamaStreaming(
2611
+ apiUrl,
2612
+ { model, messages: ollamaMessages, tools },
2613
+ options.onToken,
2614
+ signal,
2615
+ ),
2616
+ { signal, onRetry: options.onStreamRetry },
2574
2617
  );
2575
2618
  }
2576
2619
  const response = await fetch(apiUrl, {
@@ -2585,7 +2628,7 @@ export async function chatWithTools(rawMessages, options) {
2585
2628
  }),
2586
2629
  });
2587
2630
  if (!response.ok) {
2588
- throw new Error(`Ollama error: ${response.status}`);
2631
+ throw new Error(formatProviderHttpError("ollama", response.status));
2589
2632
  }
2590
2633
  const data = await response.json();
2591
2634
  if (data.prompt_eval_count || data.eval_count) {
@@ -2652,13 +2695,17 @@ export async function chatWithTools(rawMessages, options) {
2652
2695
  // and forward text deltas live, assembling tool_use blocks back into the
2653
2696
  // same {message, usage} shape the non-streaming path returns.
2654
2697
  if (typeof options.onToken === "function") {
2655
- return await _chatAnthropicStreaming(
2656
- `${url}/messages`,
2657
- { ...body, stream: true },
2658
- { "x-api-key": key, "anthropic-version": "2023-06-01" },
2659
- options.onToken,
2660
- signal,
2661
- options.onThinking,
2698
+ return await _retryStreamingChat(
2699
+ () =>
2700
+ _chatAnthropicStreaming(
2701
+ `${url}/messages`,
2702
+ { ...body, stream: true },
2703
+ { "x-api-key": key, "anthropic-version": "2023-06-01" },
2704
+ options.onToken,
2705
+ signal,
2706
+ options.onThinking,
2707
+ ),
2708
+ { signal, onRetry: options.onStreamRetry },
2662
2709
  );
2663
2710
  }
2664
2711
 
@@ -2674,7 +2721,7 @@ export async function chatWithTools(rawMessages, options) {
2674
2721
  });
2675
2722
 
2676
2723
  if (!response.ok) {
2677
- throw new Error(`Anthropic error: ${response.status}`);
2724
+ throw new Error(formatProviderHttpError("anthropic", response.status));
2678
2725
  }
2679
2726
 
2680
2727
  const data = await response.json();
@@ -2736,19 +2783,23 @@ export async function chatWithTools(rawMessages, options) {
2736
2783
  // stream the SSE response, forward content deltas live, and reassemble the
2737
2784
  // delta-fragmented tool_calls into the standard {message, usage} shape.
2738
2785
  if (typeof options.onToken === "function") {
2739
- return await _chatOpenAIStreaming(
2740
- `${url}/chat/completions`,
2741
- {
2742
- model: model || defaultModels[provider] || "gpt-4o-mini",
2743
- messages,
2744
- tools,
2745
- stream: true,
2746
- stream_options: { include_usage: true },
2747
- },
2748
- key,
2749
- options.onToken,
2750
- signal,
2751
- provider,
2786
+ return await _retryStreamingChat(
2787
+ () =>
2788
+ _chatOpenAIStreaming(
2789
+ `${url}/chat/completions`,
2790
+ {
2791
+ model: model || defaultModels[provider] || "gpt-4o-mini",
2792
+ messages,
2793
+ tools,
2794
+ stream: true,
2795
+ stream_options: { include_usage: true },
2796
+ },
2797
+ key,
2798
+ options.onToken,
2799
+ signal,
2800
+ provider,
2801
+ ),
2802
+ { signal, onRetry: options.onStreamRetry },
2752
2803
  );
2753
2804
  }
2754
2805
 
@@ -2767,7 +2818,7 @@ export async function chatWithTools(rawMessages, options) {
2767
2818
  });
2768
2819
 
2769
2820
  if (!response.ok) {
2770
- throw new Error(`${provider} API error: ${response.status}`);
2821
+ throw new Error(formatProviderHttpError(provider, response.status));
2771
2822
  }
2772
2823
 
2773
2824
  const data = await response.json();
@@ -2880,6 +2931,108 @@ export function _streamErrorDisposition(err, signal, partialText) {
2880
2931
  return "rethrow";
2881
2932
  }
2882
2933
 
2934
+ /**
2935
+ * Format a provider HTTP error with an actionable hint. 401/403 almost always
2936
+ * means a missing/invalid API key for the ACTIVE provider — and because the
2937
+ * provider is resolved from config, a surprise "anthropic 401" usually means
2938
+ * the effective provider differs from what the user configured. Name the
2939
+ * provider and point at the fix instead of dumping a bare status code. Pure +
2940
+ * exported for tests.
2941
+ */
2942
+ export function formatProviderHttpError(provider, status) {
2943
+ const base = `${provider} API error: HTTP ${status}`;
2944
+ if (status === 401 || status === 403) {
2945
+ return (
2946
+ `${base} — authentication failed: the API key for provider "${provider}" ` +
2947
+ `is missing or invalid. Check "cc config get llm.provider" and ` +
2948
+ `"cc config get llm.apiKey" (or run Configure LLM). A surprise "${provider}" ` +
2949
+ `here usually means the effective provider differs from the one you configured.`
2950
+ );
2951
+ }
2952
+ if (status === 429) return `${base} — rate limited; please retry shortly.`;
2953
+ return base;
2954
+ }
2955
+
2956
+ /**
2957
+ * Is this error from a streaming chat request a transient API CONNECTION drop
2958
+ * that is safe to retry? True only for genuine network failures (reset /
2959
+ * timeout / DNS / refused / socket hangup / undici "terminated" / "fetch
2960
+ * failed"). False for user aborts and for HTTP/status errors (a 4xx/auth/5xx is
2961
+ * the server's verdict carried in the message, not a dropped pipe — retrying a
2962
+ * connection that never dropped won't help and could double-bill).
2963
+ *
2964
+ * Safe to act on at the dispatch seam because any error that propagates OUT of
2965
+ * `_chat*Streaming` is either an abort or a drop with ZERO output already
2966
+ * streamed (partial-output drops are preserved internally and never throw) — so
2967
+ * a retry can never duplicate visible text. Pure + exported for tests.
2968
+ */
2969
+ export function _isRetryableStreamError(err, signal) {
2970
+ if (isAbortError(err)) return false;
2971
+ if (signal && signal.aborted) return false;
2972
+ if (!err) return false;
2973
+ const code = String(err.code || err.cause?.code || "").toUpperCase();
2974
+ if (
2975
+ [
2976
+ "ECONNRESET",
2977
+ "ETIMEDOUT",
2978
+ "ECONNREFUSED",
2979
+ "EAI_AGAIN",
2980
+ "EPIPE",
2981
+ "ENETUNREACH",
2982
+ "ENOTFOUND",
2983
+ "UND_ERR_SOCKET",
2984
+ ].includes(code)
2985
+ ) {
2986
+ return true;
2987
+ }
2988
+ const msg = String(err.message || err).toLowerCase();
2989
+ return /econnreset|etimedout|econnrefused|eai_again|socket hang ?up|terminated|fetch failed|network error|timed?\s*out|premature close/.test(
2990
+ msg,
2991
+ );
2992
+ }
2993
+
2994
+ /** Bounded auto-retry for streaming connection drops (Claude-Code 2.1.181). */
2995
+ const STREAM_RETRY_MAX = 2;
2996
+ const STREAM_RETRY_BASE_MS = 400;
2997
+
2998
+ /**
2999
+ * Run a streaming chat attempt with bounded auto-retry on transient API
3000
+ * connection drops (Claude-Code 2.1.181: "auto-retry for API connection drops
3001
+ * during thinking"). Only connection-level failures are retried (see
3002
+ * `_isRetryableStreamError`); user aborts and HTTP/status errors surface
3003
+ * immediately. Backoff is exponential and abort-aware. Transparent to the
3004
+ * caller: on success returns the attempt's result; on exhaustion rethrows the
3005
+ * last error — strictly better than today (one drop → instant error).
3006
+ *
3007
+ * @param {() => Promise<any>} streamFn invokes one `_chat*Streaming` attempt
3008
+ * @param {object} opts { signal?, retries?, baseDelayMs?, onRetry?, sleep? }
3009
+ */
3010
+ export async function _retryStreamingChat(streamFn, opts = {}) {
3011
+ const retries = opts.retries ?? STREAM_RETRY_MAX;
3012
+ const base = opts.baseDelayMs ?? STREAM_RETRY_BASE_MS;
3013
+ const signal = opts.signal;
3014
+ const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
3015
+ let attempt = 0;
3016
+ for (;;) {
3017
+ try {
3018
+ return await streamFn();
3019
+ } catch (err) {
3020
+ if (attempt >= retries || !_isRetryableStreamError(err, signal))
3021
+ throw err;
3022
+ attempt++;
3023
+ if (typeof opts.onRetry === "function") {
3024
+ try {
3025
+ opts.onRetry(attempt, err);
3026
+ } catch {
3027
+ /* the retry notice is best-effort; never let it mask the retry */
3028
+ }
3029
+ }
3030
+ await sleep(base * Math.pow(2, attempt - 1));
3031
+ if (signal && signal.aborted) throw err; // user bailed during backoff
3032
+ }
3033
+ }
3034
+ }
3035
+
2883
3036
  /**
2884
3037
  * Finalize a partial stream into the standard {message, usage} shape after a
2885
3038
  * mid-stream connection drop: marks the message truncated and drops any
@@ -2902,7 +3055,7 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
2902
3055
  body: JSON.stringify({ ...body, stream: true }),
2903
3056
  });
2904
3057
  if (!response.ok) {
2905
- throw new Error(`Ollama error: ${response.status}`);
3058
+ throw new Error(formatProviderHttpError("ollama", response.status));
2906
3059
  }
2907
3060
  const state = _ollamaInitState();
2908
3061
  const reader = response.body.getReader();
@@ -3064,7 +3217,7 @@ async function _chatAnthropicStreaming(
3064
3217
  body: JSON.stringify(body),
3065
3218
  });
3066
3219
  if (!response.ok) {
3067
- throw new Error(`Anthropic error: ${response.status}`);
3220
+ throw new Error(formatProviderHttpError("anthropic", response.status));
3068
3221
  }
3069
3222
  const state = _anthropicInitState();
3070
3223
  const reader = response.body.getReader();
@@ -3184,7 +3337,7 @@ async function _chatOpenAIStreaming(
3184
3337
  body: JSON.stringify(body),
3185
3338
  });
3186
3339
  if (!response.ok) {
3187
- throw new Error(`${provider} API error: ${response.status}`);
3340
+ throw new Error(formatProviderHttpError(provider, response.status));
3188
3341
  }
3189
3342
  const state = _openaiInitState();
3190
3343
  const reader = response.body.getReader();