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
@@ -92,48 +92,79 @@ export function makeRunnableProviderFallback(
92
92
  chatFn,
93
93
  { env = process.env, onFallback } = {},
94
94
  ) {
95
+ // Notify at most once per from→to pair: in an agent loop the same fallback
96
+ // would otherwise re-warn on every turn.
97
+ const notified = new Set();
95
98
  const notify = (info) => {
96
- if (typeof onFallback === "function") {
97
- try {
98
- onFallback(info);
99
- } catch {
100
- /* notification is best-effort */
101
- }
99
+ if (typeof onFallback !== "function") return;
100
+ const key = `${info.from}→${info.to}`;
101
+ if (notified.has(key)) return;
102
+ notified.add(key);
103
+ try {
104
+ onFallback(info);
105
+ } catch {
106
+ /* notification is best-effort */
102
107
  }
103
108
  };
104
- return async function runnableProviderFallback(messages, options = {}) {
105
- try {
106
- return await chatFn(messages, options);
107
- } catch (err) {
108
- if (!isAuthError(err)) throw err;
109
- const failed = options.provider;
110
109
 
111
- // 1) The endpoint belongs to a different provider than the label.
112
- const inferred = inferProviderFromBaseUrl(options.baseUrl);
113
- if (inferred && inferred !== failed) {
114
- notify({ from: failed, to: inferred, reason: "baseurl-mismatch" });
115
- return await chatFn(messages, {
116
- ...options,
110
+ // Compute the fallback for these options WITHOUT calling the model — the
111
+ // resolution is deterministic (baseUrl-inferred provider, then env-keyed
112
+ // provider). Returns { to, reason, override } or null. Pure so we can both
113
+ // react to an auth failure AND pre-empt the known-bad attempt on later turns.
114
+ const computeFallback = (options) => {
115
+ const failed = options.provider;
116
+ const inferred = inferProviderFromBaseUrl(options.baseUrl);
117
+ if (inferred && inferred !== failed) {
118
+ return {
119
+ to: inferred,
120
+ reason: "baseurl-mismatch",
121
+ override: {
117
122
  provider: inferred,
118
123
  model: defaultModelFor(inferred) || options.model,
119
- });
120
- }
121
-
122
- // 2) Another provider has a usable key in the environment.
123
- for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {
124
- if (name === failed) continue;
125
- if (def.apiKeyEnv && nonEmpty(env[def.apiKeyEnv])) {
126
- notify({ from: failed, to: name, reason: "env-key" });
127
- return await chatFn(messages, {
128
- ...options,
124
+ },
125
+ };
126
+ }
127
+ for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {
128
+ if (name === failed) continue;
129
+ if (def.apiKeyEnv && nonEmpty(env[def.apiKeyEnv])) {
130
+ return {
131
+ to: name,
132
+ reason: "env-key",
133
+ override: {
129
134
  provider: name,
130
135
  baseUrl: def.baseUrl,
131
136
  apiKey: envKey(name, env),
132
137
  model: defaultModelFor(name) || options.model,
133
- });
134
- }
138
+ },
139
+ };
135
140
  }
136
- throw err; // nowhere runnable to fall back to — surface the clear error
141
+ }
142
+ return null;
143
+ };
144
+
145
+ // Providers we have already proven need a fallback — later turns route
146
+ // straight to the working provider instead of re-attempting the broken one.
147
+ const stickyFrom = new Set();
148
+
149
+ return async function runnableProviderFallback(messages, options = {}) {
150
+ // Known-bad provider: skip the wasted failed attempt and route directly.
151
+ if (stickyFrom.has(options.provider)) {
152
+ const fb = computeFallback(options);
153
+ if (fb) {
154
+ notify({ from: options.provider, to: fb.to, reason: fb.reason });
155
+ return await chatFn(messages, { ...options, ...fb.override });
156
+ }
157
+ // Resolution no longer available — fall through to a normal attempt.
158
+ }
159
+ try {
160
+ return await chatFn(messages, options);
161
+ } catch (err) {
162
+ if (!isAuthError(err)) throw err;
163
+ const fb = computeFallback(options);
164
+ if (!fb) throw err; // nowhere runnable — surface the clear error
165
+ stickyFrom.add(options.provider);
166
+ notify({ from: options.provider, to: fb.to, reason: fb.reason });
167
+ return await chatFn(messages, { ...options, ...fb.override });
137
168
  }
138
169
  };
139
170
  }
@@ -49,8 +49,23 @@ export function extractUsage(event) {
49
49
  const totalTokens = toNumber(
50
50
  raw.total_tokens ?? raw.totalTokens ?? inputTokens + outputTokens,
51
51
  );
52
+ // Prompt-cache token counts (Anthropic caching): read tokens are billed at
53
+ // ~10% and creation/write at ~1.25× the input rate, so they are tracked
54
+ // separately for accurate cost. Absent on non-caching providers → 0.
55
+ const cacheReadTokens = toNumber(
56
+ raw.cache_read_input_tokens ?? raw.cacheReadTokens ?? 0,
57
+ );
58
+ const cacheCreationTokens = toNumber(
59
+ raw.cache_creation_input_tokens ?? raw.cacheCreationTokens ?? 0,
60
+ );
52
61
 
53
- if (inputTokens === 0 && outputTokens === 0 && totalTokens === 0) {
62
+ if (
63
+ inputTokens === 0 &&
64
+ outputTokens === 0 &&
65
+ totalTokens === 0 &&
66
+ cacheReadTokens === 0 &&
67
+ cacheCreationTokens === 0
68
+ ) {
54
69
  return null;
55
70
  }
56
71
 
@@ -60,6 +75,8 @@ export function extractUsage(event) {
60
75
  inputTokens,
61
76
  outputTokens,
62
77
  totalTokens,
78
+ cacheReadTokens,
79
+ cacheCreationTokens,
63
80
  timestamp: event.timestamp || null,
64
81
  };
65
82
  }
@@ -73,6 +90,8 @@ export function aggregateUsage(events) {
73
90
  inputTokens: 0,
74
91
  outputTokens: 0,
75
92
  totalTokens: 0,
93
+ cacheReadTokens: 0,
94
+ cacheCreationTokens: 0,
76
95
  calls: 0,
77
96
  };
78
97
 
@@ -83,6 +102,8 @@ export function aggregateUsage(events) {
83
102
  total.inputTokens += u.inputTokens;
84
103
  total.outputTokens += u.outputTokens;
85
104
  total.totalTokens += u.totalTokens;
105
+ total.cacheReadTokens += u.cacheReadTokens || 0;
106
+ total.cacheCreationTokens += u.cacheCreationTokens || 0;
86
107
  total.calls += 1;
87
108
 
88
109
  const key = `${u.provider || "?"}/${u.model || "?"}`;
@@ -92,11 +113,15 @@ export function aggregateUsage(events) {
92
113
  inputTokens: 0,
93
114
  outputTokens: 0,
94
115
  totalTokens: 0,
116
+ cacheReadTokens: 0,
117
+ cacheCreationTokens: 0,
95
118
  calls: 0,
96
119
  };
97
120
  entry.inputTokens += u.inputTokens;
98
121
  entry.outputTokens += u.outputTokens;
99
122
  entry.totalTokens += u.totalTokens;
123
+ entry.cacheReadTokens += u.cacheReadTokens || 0;
124
+ entry.cacheCreationTokens += u.cacheCreationTokens || 0;
100
125
  entry.calls += 1;
101
126
  byKey.set(key, entry);
102
127
  }
@@ -129,6 +154,8 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
129
154
  inputTokens: 0,
130
155
  outputTokens: 0,
131
156
  totalTokens: 0,
157
+ cacheReadTokens: 0,
158
+ cacheCreationTokens: 0,
132
159
  calls: 0,
133
160
  };
134
161
  const byKey = new Map();
@@ -137,6 +164,8 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
137
164
  total.inputTokens += s.total.inputTokens;
138
165
  total.outputTokens += s.total.outputTokens;
139
166
  total.totalTokens += s.total.totalTokens;
167
+ total.cacheReadTokens += s.total.cacheReadTokens || 0;
168
+ total.cacheCreationTokens += s.total.cacheCreationTokens || 0;
140
169
  total.calls += s.total.calls;
141
170
  for (const row of s.byModel) {
142
171
  const key = `${row.provider || "?"}/${row.model || "?"}`;
@@ -146,11 +175,15 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
146
175
  inputTokens: 0,
147
176
  outputTokens: 0,
148
177
  totalTokens: 0,
178
+ cacheReadTokens: 0,
179
+ cacheCreationTokens: 0,
149
180
  calls: 0,
150
181
  };
151
182
  entry.inputTokens += row.inputTokens;
152
183
  entry.outputTokens += row.outputTokens;
153
184
  entry.totalTokens += row.totalTokens;
185
+ entry.cacheReadTokens += row.cacheReadTokens || 0;
186
+ entry.cacheCreationTokens += row.cacheCreationTokens || 0;
154
187
  entry.calls += row.calls;
155
188
  byKey.set(key, entry);
156
189
  }
@@ -199,7 +199,10 @@ export function substituteArguments(body, args) {
199
199
  // Protect literal $$
200
200
  const MARKER = "\u0000DOLLAR\u0000";
201
201
  let out = body.replace(/\$\$/g, MARKER);
202
- out = out.replace(/\$ARGUMENTS\b/g, full);
202
+ // Insert the args via a function so $-sequences in the user's arguments
203
+ // ($&, $`, $', $$) are placed literally instead of being interpreted as
204
+ // String.replace patterns (which would otherwise mangle the args).
205
+ out = out.replace(/\$ARGUMENTS\b/g, () => full);
203
206
  out = out.replace(/\$(\d+)/g, (match, idx) => {
204
207
  const i = parseInt(idx, 10) - 1;
205
208
  if (i < 0 || i >= positional.length) return match;
@@ -158,7 +158,7 @@ ${commandDocs}
158
158
  /**
159
159
  * Generate handler.js content for a direct-execution pack
160
160
  */
161
- function generateDirectHandler(domainKey, domainDef) {
161
+ export function generateDirectHandler(domainKey, domainDef) {
162
162
  const commandList = Object.keys(domainDef.commands).join('", "');
163
163
  const commandGuide = Object.entries(domainDef.commands)
164
164
  .map(
@@ -241,6 +241,17 @@ ${commandGuide}
241
241
  const useJson = JSON_SUPPORTED_COMMANDS.has(command) && !cliArgs.includes("--json");
242
242
  if (useJson) cliArgs.push("--json");
243
243
 
244
+ // Security: spawnSync uses shell:true (needed to run the chainlesschain.cmd
245
+ // shim on Windows), which joins cliArgs into a shell command. Reject shell
246
+ // metacharacters so a crafted skill input can't inject commands.
247
+ const unsafeArg = cliArgs.find((a) => /[;&|\`$<>()\\n\\r]/.test(a));
248
+ if (unsafeArg) {
249
+ return {
250
+ success: false,
251
+ error: "参数包含不安全的 shell 字符,已拒绝执行: " + JSON.stringify(unsafeArg),
252
+ };
253
+ }
254
+
244
255
  // Execute via child process
245
256
  const result = spawnSync("chainlesschain", cliArgs, {
246
257
  encoding: "utf-8",
@@ -371,7 +382,7 @@ module.exports = handler;
371
382
  /**
372
383
  * Generate handler.js content for hybrid-mode pack
373
384
  */
374
- function generateHybridHandler(domainKey, domainDef) {
385
+ export function generateHybridHandler(domainKey, domainDef) {
375
386
  const agentCmds = Object.entries(domainDef.commands)
376
387
  .filter(([cmd, info]) => info.isAgentMode || AGENT_MODE_COMMANDS.has(cmd))
377
388
  .map(([cmd]) => `"${cmd}"`)
@@ -458,6 +469,18 @@ const handler = {
458
469
 
459
470
  // Direct execution for other commands
460
471
  const cliArgs = [...args, "--quiet"];
472
+
473
+ // Security: spawnSync uses shell:true (needed to run the chainlesschain.cmd
474
+ // shim on Windows), which joins cliArgs into a shell command. Reject shell
475
+ // metacharacters so a crafted skill input can't inject commands.
476
+ const unsafeArg = cliArgs.find((a) => /[;&|\`$<>()\\n\\r]/.test(a));
477
+ if (unsafeArg) {
478
+ return {
479
+ success: false,
480
+ error: "参数包含不安全的 shell 字符,已拒绝执行: " + JSON.stringify(unsafeArg),
481
+ };
482
+ }
483
+
461
484
  const result = spawnSync("chainlesschain", cliArgs, {
462
485
  encoding: "utf-8",
463
486
  shell: true,
@@ -0,0 +1,56 @@
1
+ /**
2
+ * stream-retry — shared classification of transient streaming-chat failures.
3
+ *
4
+ * A streaming LLM call can drop mid-flight (ECONNRESET / "terminated" / socket
5
+ * hangup / DNS). When that happens BEFORE any output reached the user it is safe
6
+ * to re-issue the request. This module is the single source of truth for "is
7
+ * this error a retryable connection drop?" so the agent path (agent-core) and
8
+ * the chat path (chat-core) classify identically.
9
+ *
10
+ * Pure + dependency-light (only the shared abort-utils) so it can be unit-tested
11
+ * and imported by either streaming implementation without pulling in the heavy
12
+ * agent runtime.
13
+ */
14
+
15
+ import { isAbortError } from "./abort-utils.js";
16
+
17
+ /** Bounded auto-retry budget for streaming connection drops (Claude-Code 2.1.181). */
18
+ export const STREAM_RETRY_MAX = 2;
19
+ export const STREAM_RETRY_BASE_MS = 400;
20
+
21
+ /**
22
+ * Is this error from a streaming chat request a transient API CONNECTION drop
23
+ * that is safe to retry? True only for genuine network failures (reset /
24
+ * timeout / DNS / refused / socket hangup / undici "terminated" / "fetch
25
+ * failed"). False for user aborts and for HTTP/status errors (a 4xx/auth/5xx is
26
+ * the server's verdict carried in the message, not a dropped pipe — retrying a
27
+ * connection that never dropped won't help and could double-bill).
28
+ *
29
+ * @param {unknown} err
30
+ * @param {AbortSignal} [signal] the caller's signal — a user abort is never retryable
31
+ * @returns {boolean}
32
+ */
33
+ export function isRetryableStreamError(err, signal) {
34
+ if (isAbortError(err)) return false;
35
+ if (signal && signal.aborted) return false;
36
+ if (!err) return false;
37
+ const code = String(err.code || err.cause?.code || "").toUpperCase();
38
+ if (
39
+ [
40
+ "ECONNRESET",
41
+ "ETIMEDOUT",
42
+ "ECONNREFUSED",
43
+ "EAI_AGAIN",
44
+ "EPIPE",
45
+ "ENETUNREACH",
46
+ "ENOTFOUND",
47
+ "UND_ERR_SOCKET",
48
+ ].includes(code)
49
+ ) {
50
+ return true;
51
+ }
52
+ const msg = String(err.message || err).toLowerCase();
53
+ return /econnreset|etimedout|econnrefused|eai_again|socket hang ?up|terminated|fetch failed|network error|timed?\s*out|premature close/.test(
54
+ msg,
55
+ );
56
+ }
@@ -23,6 +23,7 @@ import fs from "node:fs";
23
23
  import path from "node:path";
24
24
  import os from "node:os";
25
25
  import crypto from "node:crypto";
26
+ import { withFileLock } from "./with-file-lock.js";
26
27
 
27
28
  const SENSITIVE_FIELDS = ["sync.webdav.password", "sync.oss.secretAccessKey"];
28
29
  const ALLOWED_PROVIDER_IDS = ["webdav", "oss"];
@@ -55,6 +56,37 @@ function _ensureDir() {
55
56
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
56
57
  }
57
58
 
59
+ /**
60
+ * Write a secret file atomically. A crash (or concurrent writer) mid-write must
61
+ * never corrupt the master key or the encrypted vault — a truncated key throws
62
+ * on every load, a truncated vault fails AEAD decrypt and forces a reconfigure.
63
+ * Temp sibling + rename (atomic within a filesystem); when `mode` is given the
64
+ * temp is created with it (0600) so the secret is never world-readable in the
65
+ * window before rename. chmod after rename is belt-and-suspenders (best-effort
66
+ * on NTFS / older fs).
67
+ */
68
+ function _atomicWriteFileSync(filePath, data, mode) {
69
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
70
+ try {
71
+ fs.writeFileSync(tmp, data, mode != null ? { mode } : undefined);
72
+ fs.renameSync(tmp, filePath);
73
+ if (mode != null) {
74
+ try {
75
+ fs.chmodSync(filePath, mode);
76
+ } catch (_e) {
77
+ /* non-fatal: NTFS / older fs */
78
+ }
79
+ }
80
+ } catch (err) {
81
+ try {
82
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
83
+ } catch {
84
+ /* best-effort temp cleanup */
85
+ }
86
+ throw err;
87
+ }
88
+ }
89
+
58
90
  function _loadOrCreateMasterKey() {
59
91
  _ensureDir();
60
92
  const kp = _keyPath();
@@ -69,12 +101,10 @@ function _loadOrCreateMasterKey() {
69
101
  return buf;
70
102
  }
71
103
  const key = crypto.randomBytes(KEY_LEN);
72
- fs.writeFileSync(kp, key);
73
- try {
74
- fs.chmodSync(kp, 0o600);
75
- } catch (_e) {
76
- /* non-fatal: NTFS / older fs */
77
- }
104
+ // Atomic + 0600 from creation: a crash mid-write must not leave a truncated
105
+ // master key (a wrong-length key file then throws on every load), and the
106
+ // secret must never be briefly world-readable in the gap before chmod.
107
+ _atomicWriteFileSync(kp, key, 0o600);
78
108
  return key;
79
109
  }
80
110
 
@@ -123,7 +153,7 @@ function loadAll() {
123
153
 
124
154
  function saveAll(all) {
125
155
  _ensureDir();
126
- fs.writeFileSync(_vaultPath(), _encryptBlob(JSON.stringify(all)));
156
+ _atomicWriteFileSync(_vaultPath(), _encryptBlob(JSON.stringify(all)));
127
157
  return true;
128
158
  }
129
159
 
@@ -184,20 +214,26 @@ function setCredentials(providerId, creds) {
184
214
  if (!creds || typeof creds !== "object") {
185
215
  throw new Error("sync-credentials: creds must be an object");
186
216
  }
187
- const all = loadAll();
188
- if (!all.sync || typeof all.sync !== "object") all.sync = {};
189
- all.sync[providerId] = { ...creds };
190
- return saveAll(all);
217
+ // Serialize the decrypt-modify-encrypt across processes so a concurrent write
218
+ // to a different provider doesn't drop this one (best-effort, never hangs).
219
+ return withFileLock(_vaultPath(), () => {
220
+ const all = loadAll();
221
+ if (!all.sync || typeof all.sync !== "object") all.sync = {};
222
+ all.sync[providerId] = { ...creds };
223
+ return saveAll(all);
224
+ });
191
225
  }
192
226
 
193
227
  function clearCredentials(providerId) {
194
228
  assertProviderId(providerId);
195
- const all = loadAll();
196
- if (all?.sync?.[providerId]) {
197
- delete all.sync[providerId];
198
- return saveAll(all);
199
- }
200
- return true;
229
+ return withFileLock(_vaultPath(), () => {
230
+ const all = loadAll();
231
+ if (all?.sync?.[providerId]) {
232
+ delete all.sync[providerId];
233
+ return saveAll(all);
234
+ }
235
+ return true;
236
+ });
201
237
  }
202
238
 
203
239
  /** Test seam: override the resolved chainlesschain dir without env leak. */
@@ -21,9 +21,19 @@ const PRICING = {
21
21
  _default: { input: 2.5, output: 10 },
22
22
  },
23
23
  anthropic: {
24
+ // Current Anthropic list prices (USD/1M), verified 2026-06-21 vs the
25
+ // claude-api reference. Lookup is EXACT-key (calculateCost), so keep current
26
+ // model ids listed or they fall back to the Sonnet _default and mis-price —
27
+ // e.g. claude-opus-4-8 would otherwise be billed at $3/$15 not $5/$25. The
28
+ // Opus tier dropped to $5/$25 with Opus 4.5; Haiku 4.5 is $1/$5.
29
+ "claude-opus-4-8": { input: 5, output: 25 },
30
+ "claude-opus-4-7": { input: 5, output: 25 },
31
+ "claude-opus-4-6": { input: 5, output: 25 },
32
+ "claude-opus-4-5": { input: 5, output: 25 },
24
33
  "claude-sonnet-4-6": { input: 3, output: 15 },
25
- "claude-opus-4-6": { input: 15, output: 75 },
26
- "claude-haiku-4-5-20251001": { input: 0.8, output: 4 },
34
+ "claude-haiku-4-5": { input: 1, output: 5 },
35
+ "claude-haiku-4-5-20251001": { input: 1, output: 5 },
36
+ "claude-fable-5": { input: 10, output: 50 },
27
37
  _default: { input: 3, output: 15 },
28
38
  },
29
39
  deepseek: {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Best-effort cross-process advisory lock around a state file.
3
+ *
4
+ * Multiple `cc` processes (and, for some dirs, the desktop app) can do
5
+ * read-modify-write on the same JSON state file. An atomic temp+rename makes
6
+ * each write crash-safe but does NOT prevent a lost update: A reads, B reads, A
7
+ * writes, B writes → A's change is gone. This serializes the whole
8
+ * read-mutate-write across processes using a lock DIRECTORY — `mkdir` is
9
+ * atomic-exclusive on POSIX and Windows, so exactly one process holds it.
10
+ *
11
+ * SAFETY — this is intentionally best-effort. If the lock can't be acquired
12
+ * within `timeoutMs`, it PROCEEDS WITHOUT THE LOCK rather than hanging or
13
+ * throwing: worst case degrades to the pre-lock behavior (a rare lost update),
14
+ * never a frozen CLI. A lock left by a crashed holder is reclaimed once its
15
+ * mtime is older than `staleMs`. The lock is always released in `finally`.
16
+ *
17
+ * @param {string} targetPath the file being guarded (lock is `${targetPath}.lock`)
18
+ * @param {(ctx:{locked:boolean})=>T} fn critical section; receives whether the lock was held
19
+ * @returns {T} whatever `fn` returns
20
+ */
21
+ export function withFileLock(targetPath, fn, opts = {}) {
22
+ const {
23
+ timeoutMs = 2000,
24
+ staleMs = 30000,
25
+ retryMs = 25,
26
+ _fs = defaultFs,
27
+ _now = () => Date.now(),
28
+ _sleep = sleepSync,
29
+ } = opts;
30
+
31
+ const lockDir = `${targetPath}.lock`;
32
+ let held = false;
33
+ const deadline = _now() + timeoutMs;
34
+
35
+ for (;;) {
36
+ try {
37
+ _fs.mkdirSync(lockDir);
38
+ held = true;
39
+ break;
40
+ } catch (err) {
41
+ if (!err || err.code !== "EEXIST") break; // unexpected fs error → run unlocked
42
+ // Reclaim a stale lock left by a crashed holder.
43
+ try {
44
+ const age = _now() - _fs.statSync(lockDir).mtimeMs;
45
+ if (age > staleMs) {
46
+ _fs.rmSync(lockDir, { recursive: true, force: true });
47
+ continue; // retry acquisition immediately
48
+ }
49
+ } catch {
50
+ /* stat lost a race with the holder releasing — fall through to wait */
51
+ }
52
+ if (_now() >= deadline) break; // timed out → run unlocked (best-effort)
53
+ _sleep(retryMs);
54
+ }
55
+ }
56
+
57
+ try {
58
+ return fn({ locked: held });
59
+ } finally {
60
+ if (held) {
61
+ try {
62
+ _fs.rmSync(lockDir, { recursive: true, force: true });
63
+ } catch {
64
+ /* best-effort release */
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ import fs from "node:fs";
71
+ const defaultFs = {
72
+ mkdirSync: (p) => fs.mkdirSync(p),
73
+ statSync: (p) => fs.statSync(p),
74
+ rmSync: (p, o) => fs.rmSync(p, o),
75
+ };
76
+
77
+ /** Synchronous sleep that doesn't busy-spin the CPU when possible. */
78
+ function sleepSync(ms) {
79
+ try {
80
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
81
+ } catch {
82
+ const end = Date.now() + ms; // SharedArrayBuffer unavailable — bounded spin
83
+ while (Date.now() < end) {
84
+ /* spin */
85
+ }
86
+ }
87
+ }
@@ -286,9 +286,19 @@ export async function startAgentRepl(options = {}) {
286
286
  // an auto-detected image path so the REPL switches to a vision-capable model
287
287
  // for that turn only (resolveVisionLlm falls back to the default when unset).
288
288
  let _visionModel;
289
+ // Hard stream inactivity timeout override (config.llm.streamStallTimeoutMs,
290
+ // ms): a stream silent that long is aborted + retried instead of hanging.
291
+ // Unset → agent-core's 180s default (matches cc chat/ask). Set to 0 to
292
+ // disable. Left undefined here means "use the default".
293
+ let _streamStallTimeoutMs;
289
294
  try {
290
295
  const { loadConfig } = await import("../lib/config-manager.js");
291
- _visionModel = loadConfig()?.llm?.visionModel || undefined;
296
+ const _cfg = loadConfig();
297
+ _visionModel = _cfg?.llm?.visionModel || undefined;
298
+ const raw = _cfg?.llm?.streamStallTimeoutMs;
299
+ const t = Number(raw);
300
+ // Accept 0 (explicit disable) — only ignore absent/invalid values.
301
+ if (raw != null && Number.isFinite(t) && t >= 0) _streamStallTimeoutMs = t;
292
302
  } catch {
293
303
  /* optional — resolveVisionLlm falls back to DEFAULT_VISION_MODEL */
294
304
  }
@@ -721,6 +731,7 @@ export async function startAgentRepl(options = {}) {
721
731
  // an IDE integrated terminal. --ide forces it, --no-ide disables it
722
732
  // (parity with headless; auto-detect already works via process.env).
723
733
  ide: options.ide,
734
+ pdh: options.pdh,
724
735
  cwd: process.cwd(),
725
736
  // advertise the session id to spawned stdio MCP servers
726
737
  sessionId,
@@ -2463,12 +2474,19 @@ export async function startAgentRepl(options = {}) {
2463
2474
  process.stdout.write(chalk.gray(` [step] ${evt.step}\n`));
2464
2475
  };
2465
2476
 
2466
- autonomousAgent.on("goal:completed", goalListener);
2467
- autonomousAgent.on("goal:failed", goalListener);
2468
- autonomousAgent.on("step:started", stepListener);
2469
- autonomousAgent.on("step:completed", (evt) => {
2470
- process.stdout.write(chalk.green(` [done] ${evt.step}\n`));
2471
- });
2477
+ // Attach the live-output listeners ONCE per REPL session — the agent is
2478
+ // a session-scoped singleton (created once above), so re-attaching on
2479
+ // every `/auto` would accumulate listeners (duplicate output + leak +
2480
+ // MaxListenersExceededWarning). The handlers are stateless.
2481
+ if (!autonomousAgent._replListenersAttached) {
2482
+ autonomousAgent._replListenersAttached = true;
2483
+ autonomousAgent.on("goal:completed", goalListener);
2484
+ autonomousAgent.on("goal:failed", goalListener);
2485
+ autonomousAgent.on("step:started", stepListener);
2486
+ autonomousAgent.on("step:completed", (evt) => {
2487
+ process.stdout.write(chalk.green(` [done] ${evt.step}\n`));
2488
+ });
2489
+ }
2472
2490
 
2473
2491
  logger.info(`Submitting goal: ${chalk.cyan(autoArg)}`);
2474
2492
  try {
@@ -3249,6 +3267,28 @@ export async function startAgentRepl(options = {}) {
3249
3267
  ` ⟳ connection dropped — retrying (attempt ${attempt})…\n`,
3250
3268
  ),
3251
3269
  ),
3270
+ // Stream-stall hint (Claude-Code 2.1.185): the connection is alive but
3271
+ // the API has gone silent mid-response — tell the user we're still
3272
+ // waiting instead of leaving a frozen spinner. stderr so it never
3273
+ // corrupts the streamed answer on stdout.
3274
+ onStall: (ms, timeoutMs) => {
3275
+ const silent = Math.round(ms / 1000);
3276
+ // 2.1.185: when a hard inactivity timeout is set, tell the user when
3277
+ // the stalled stream will auto-retry instead of leaving them unsure
3278
+ // whether it's hung forever.
3279
+ const retryIn =
3280
+ timeoutMs > ms ? Math.round((timeoutMs - ms) / 1000) : 0;
3281
+ const suffix = retryIn > 0 ? ` · will retry in ${retryIn}s` : "";
3282
+ process.stderr.write(
3283
+ chalk.dim(
3284
+ ` ⏳ waiting for API response (silent ${silent}s)${suffix}…\n`,
3285
+ ),
3286
+ );
3287
+ },
3288
+ // Hard inactivity timeout: abort + retry a dead-but-open stream instead
3289
+ // of hanging forever. undefined → agent-core's 180s default (matches cc
3290
+ // chat/ask); config.llm.streamStallTimeoutMs tunes or disables (0).
3291
+ streamStallTimeoutMs: _streamStallTimeoutMs,
3252
3292
  signal: _turnAbort.signal,
3253
3293
  // On an auto-detected image turn, switch to the vision LLM for this
3254
3294
  // turn only (provider/baseUrl/apiKey unchanged, model → vision model).