chainlesschain 0.166.109 → 0.166.114

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 (207) hide show
  1. package/README.md +14 -6
  2. package/package.json +3 -2
  3. package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
  4. package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
  5. package/src/assets/web-panel/.build-hash +1 -1
  6. package/src/assets/web-panel/assets/{AIOps-DkrNbDzS.js → AIOps-CVac3rIN.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-DJjFXDhv.js → ActionButton-ChM9GxMl.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-DyUbagRn.js → Analytics-CV5399Js.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-p7kJWSvn.js → AppLayout-CuonVXhg.js} +3 -3
  10. package/src/assets/web-panel/assets/{Artifacts-C3rVh4Ak.js → Artifacts-D9qKfgb6.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-Bch_L_YO.js → Audit-DUvLysQc.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-BNHy8zP6.js → BackgroundAgents-DfKohQeT.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-BUaye_Ys.js → Backup-CohoEI8R.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-D49mzAVz.js → BaseInput-BwdoA3xv.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-2MDzM_us.js → Chat-DRkuz1d2.js} +5 -5
  16. package/src/assets/web-panel/assets/ChatBubbleRenderer-v3pzeHNC.js +1 -0
  17. package/src/assets/web-panel/assets/{Checkbox-5p1T_KEq.js → Checkbox-B-GNGpKv.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-TKtiLaoZ.js → Codegen-Bs2lAsoZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-BVCMG9mS.js → Col-Bk1mJKDZ.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-D09-QczH.js → Community-B_FDSCOo.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-QuRMCnj7.js → Compact-No1qSLLH.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-CN0PV7fk.js → Compliance-CcrG43-d.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-5N2zhAty.js → Cowork-DLkj2ywK.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-BxT8UZ0S.js → Cron-sQHELVEz.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-Dp-noQl4.js → Crosschain-CRTZ3GZg.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-B7MZFkjG.js → DID-Ds2bjvxE.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-DulJ3rKV.js → Dashboard-DOONG2NX.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-DV4ov4zF.js → Dropdown-Br3PNA2L.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-DB2-ooz6.js → EmailListRenderer-DT6UUqjZ.js} +1 -1
  30. package/src/assets/web-panel/assets/{EvolutionSettings-B1Rb6p7q.js → EvolutionSettings-BSLuEjlT.js} +1 -1
  31. package/src/assets/web-panel/assets/{FamilyGuardDashboard-U8aHayx-.js → FamilyGuardDashboard-2aEM9avE.js} +1 -1
  32. package/src/assets/web-panel/assets/{Federation-BkQULBHY.js → Federation-D3B0tlQU.js} +1 -1
  33. package/src/assets/web-panel/assets/{FormItemContext-CtviG260.js → FormItemContext-DTVkVVQP.js} +1 -1
  34. package/src/assets/web-panel/assets/{GenericCardRenderer-DnPirB24.js → GenericCardRenderer-BgFEgP4y.js} +1 -1
  35. package/src/assets/web-panel/assets/{Git-CK57Oro6.js → Git-3t71m6Xd.js} +2 -2
  36. package/src/assets/web-panel/assets/{Governance-DbI-LB8-.js → Governance-DS7WvlaX.js} +1 -1
  37. package/src/assets/web-panel/assets/{Inference-sEG4T1Jg.js → Inference-CeT-xWrI.js} +1 -1
  38. package/src/assets/web-panel/assets/{KnowledgeGraph-9SLJR3h9.js → KnowledgeGraph-BQzMncp5.js} +1 -1
  39. package/src/assets/web-panel/assets/{Logs-BIZYcISQ.js → Logs-CC5DOKhV.js} +2 -2
  40. package/src/assets/web-panel/assets/{MarkdownRenderer-9nfWupWP.js → MarkdownRenderer-5jYM4kYG.js} +1 -1
  41. package/src/assets/web-panel/assets/{Marketplace-CRmLLwAE.js → Marketplace-CpYDyy3o.js} +1 -1
  42. package/src/assets/web-panel/assets/{McpTools-DkOFkSO1.js → McpTools-DRdAZPMr.js} +3 -3
  43. package/src/assets/web-panel/assets/{Memory-M29Kd9TU.js → Memory-CBLRr-l8.js} +2 -2
  44. package/src/assets/web-panel/assets/{MobileBridge-Tl6JvPw_.js → MobileBridge-Bnj9veNN.js} +1 -1
  45. package/src/assets/web-panel/assets/{MobileProjects-Bq-FPDzS.js → MobileProjects-lYFPD24p.js} +1 -1
  46. package/src/assets/web-panel/assets/{Mtc-DkUjX1F5.js → Mtc-BMU_C9vD.js} +2 -2
  47. package/src/assets/web-panel/assets/{MtcAudit-BCqr1I4q.js → MtcAudit-DQV-zumU.js} +4 -4
  48. package/src/assets/web-panel/assets/{Multisig-DmoqYGQ5.js → Multisig-LpqpqV1d.js} +3 -3
  49. package/src/assets/web-panel/assets/{NLProgramming-DNRQeRke.js → NLProgramming-C4OMklSO.js} +1 -1
  50. package/src/assets/web-panel/assets/{Notes-CQSBeiY6.js → Notes-Cg0QCinP.js} +3 -3
  51. package/src/assets/web-panel/assets/{NotificationSettings-gNY9bbH6.js → NotificationSettings-amoZoMna.js} +1 -1
  52. package/src/assets/web-panel/assets/{OrderTableRenderer-DW0CNjh8.js → OrderTableRenderer-CxNvhf5n.js} +1 -1
  53. package/src/assets/web-panel/assets/{Organization-CthnbKag.js → Organization-BetJmyX5.js} +4 -4
  54. package/src/assets/web-panel/assets/{Overflow-_GQ3sbXu.js → Overflow-DbeoPtVD.js} +1 -1
  55. package/src/assets/web-panel/assets/{P2P-BTr7ea6W.js → P2P-B5sitQWT.js} +2 -2
  56. package/src/assets/web-panel/assets/PdhVaultBrowser-AP9SG2ic.js +7 -0
  57. package/src/assets/web-panel/assets/{Permissions-DMny2Cjs.js → Permissions-Bc7fgNKx.js} +4 -4
  58. package/src/assets/web-panel/assets/{PersonalDataHub-CyYlMSoI.js → PersonalDataHub-BwdGMcWZ.js} +3 -3
  59. package/src/assets/web-panel/assets/{Pipeline-FASu2Oc4.js → Pipeline-DVuJA9BA.js} +1 -1
  60. package/src/assets/web-panel/assets/{Privacy-9bC5FnsJ.js → Privacy-BGzwOYRG.js} +1 -1
  61. package/src/assets/web-panel/assets/{ProjectInit-BOYZwB5G.js → ProjectInit-BpHcC5br.js} +2 -2
  62. package/src/assets/web-panel/assets/{ProjectSettings-45DViNG-.js → ProjectSettings-BI5G5gsc.js} +2 -2
  63. package/src/assets/web-panel/assets/{Projects-C2fZzMZc.js → Projects-Ce92-l1V.js} +1 -1
  64. package/src/assets/web-panel/assets/{Providers-BgjvZx9F.js → Providers-CRHCcLFS.js} +1 -1
  65. package/src/assets/web-panel/assets/{QrScannerModal-Brw-3csB.js → QrScannerModal-qSeg5xag.js} +1 -1
  66. package/src/assets/web-panel/assets/QuickAsk-CA8oSceR.js +1 -0
  67. package/src/assets/web-panel/assets/{Recommend-DuQGxjIR.js → Recommend-DMvPodNo.js} +1 -1
  68. package/src/assets/web-panel/assets/{RemoteSession-T1cdSSRL.js → RemoteSession-BWsapEyN.js} +2 -2
  69. package/src/assets/web-panel/assets/{Reputation-9FT2dI4G.js → Reputation-BZJETVmC.js} +1 -1
  70. package/src/assets/web-panel/assets/{Row-yNIbNftX.js → Row-CUE1OSMp.js} +1 -1
  71. package/src/assets/web-panel/assets/{RssFeed-BIchNoPd.js → RssFeed-DvMazcUD.js} +2 -2
  72. package/src/assets/web-panel/assets/{Search-msDJ8UTZ.js → Search-e1c-NXWv.js} +1 -1
  73. package/src/assets/web-panel/assets/{Security-DE46oBYT.js → Security-Dc9GZsx2.js} +3 -3
  74. package/src/assets/web-panel/assets/{Services-Dori7vc3.js → Services-AFz6ke-4.js} +2 -2
  75. package/src/assets/web-panel/assets/{Skeleton-D3O3sUFz.js → Skeleton-gow6MYcY.js} +1 -1
  76. package/src/assets/web-panel/assets/{Skills-BV0ct8SO.js → Skills-h6AxYjUP.js} +1 -1
  77. package/src/assets/web-panel/assets/{Sla-DflIVOrh.js → Sla-Cr4CkJ0O.js} +1 -1
  78. package/src/assets/web-panel/assets/{SpeechSettings-Cpbv2Zz6.js → SpeechSettings-Bd5yuBtH.js} +1 -1
  79. package/src/assets/web-panel/assets/{SyncSettings-Dgg6-wbe.js → SyncSettings-1t4QZMLW.js} +2 -2
  80. package/src/assets/web-panel/assets/{Tasks-Cb6e4Klx.js → Tasks-u-DrjLRM.js} +1 -1
  81. package/src/assets/web-panel/assets/{Templates-GfdAATGz.js → Templates-C0yLUC9b.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tenant-D0gecLXl.js → Tenant-ds1YT5iS.js} +1 -1
  83. package/src/assets/web-panel/assets/{Terminal-BFRPJt-0.js → Terminal-BoIskW2m.js} +2 -2
  84. package/src/assets/web-panel/assets/{TimelineRenderer-BobtChxX.js → TimelineRenderer-BkdVK24X.js} +1 -1
  85. package/src/assets/web-panel/assets/{Tokens-CifeZtHV.js → Tokens-djyDgmfk.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trigger-BHBvbBCf.js → Trigger-CYefDS3H.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trust-6lvqjKYp.js → Trust-CE_dbep_.js} +1 -1
  88. package/src/assets/web-panel/assets/{UkeySign-CqzMc-G5.js → UkeySign-DwVs_fey.js} +1 -1
  89. package/src/assets/web-panel/assets/{VideoEditing-OIiRG582.js → VideoEditing-BTUqMHAV.js} +1 -1
  90. package/src/assets/web-panel/assets/{Wallet-4MeKq0q_.js → Wallet-Bu_E7Rrc.js} +4 -4
  91. package/src/assets/web-panel/assets/{WebAuthn-eHBRjGQG.js → WebAuthn-DTbre5AB.js} +4 -4
  92. package/src/assets/web-panel/assets/{WorkflowEditor-DlzYdEqW.js → WorkflowEditor-BsBzd3gt.js} +1 -1
  93. package/src/assets/web-panel/assets/{chat-CBcjOzkX.js → chat-tdifQQvu.js} +1 -1
  94. package/src/assets/web-panel/assets/{colors-zgwsYQSa.js → colors-D3SNW2Vx.js} +1 -1
  95. package/src/assets/web-panel/assets/{compact-item-Dcd9H5Bn.js → compact-item-UBlCjrAW.js} +1 -1
  96. package/src/assets/web-panel/assets/{createContext-DR1tO-8r.js → createContext-DKsnEnFt.js} +1 -1
  97. package/src/assets/web-panel/assets/devWarning-BBnVfJHh.js +1 -0
  98. package/src/assets/web-panel/assets/{hasIn-CtfbUH_p.js → hasIn-BkeySXWe.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BXfGHUz_.js → index-6Nvzgytx.js} +1 -1
  100. package/src/assets/web-panel/assets/index-B2w_3mtS.js +1 -0
  101. package/src/assets/web-panel/assets/{index-DYPMJ5Mb.js → index-BEiHKah0.js} +25 -25
  102. package/src/assets/web-panel/assets/{index-LN7Hkl1C.js → index-BIeFLaMV.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DA2-Bhy8.js → index-BV2zQteq.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CwHgaYVH.js → index-BVvz3ntc.js} +1 -1
  105. package/src/assets/web-panel/assets/index-BoHurydC.js +1 -0
  106. package/src/assets/web-panel/assets/{index-DVpp9YSQ.js → index-Bw0k3VgD.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CaGNqkY0.js → index-C-KTHSUE.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DWuCGhHa.js → index-C9ImNw5O.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DTx4FyLU.js → index-CG_b1xyX.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Cur8LNNn.js → index-CUjyL0ZS.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DfzOYmOj.js → index-CXkn77ZN.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Cdhy5ckh.js → index-CZ6JUooI.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D1GxtY-h.js → index-Cuz2QCFT.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C0kP88Pb.js → index-D5Umd_Z-.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DHEiuvhA.js → index-D6es7MSG.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CPLLgPxA.js → index-D93iSDUw.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-B3QUK1ks.js → index-D9m1HQzl.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DWG98orp.js → index-D9um0YVl.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-hTcre7U9.js → index-DD6dD3LV.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-C5XGvJb0.js → index-DFMrIEIt.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DvPOpvvH.js → index-DSCiZJXb.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D8WPohye.js → index-DSe_7MSy.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-JSfldpw4.js → index-DYFjOyTH.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-5XaB_Tjg.js → index-DdyTv6yc.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BoxowMYS.js → index-DfrW-iVs.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DSvcz0RO.js → index-Dg8-Duze.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-J-ORW1i6.js → index-DiDgwRty.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CT7zIkqP.js → index-DlEIp1gJ.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-D7ZOiIKb.js → index-DlO6HdD9.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DeAi4CNt.js → index-DtxUSZSS.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-CmVIM-m2.js → index-DugZRCiI.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-oWOlF7CM.js → index-P-ailFQY.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-dvF6C39V.js → index-PWScrdlX.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-CSQCHpDo.js → index-SpQ26gD6.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-CmzQzTY5.js → index-TLBpgN30.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-z8lrkcb-.js → index-bOIcWW8F.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-BndPtS9D.js → index-pgGKNvmm.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-CVMtDrWZ.js → initDefaultProps-CVCXG2Hv.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-Dye3IU8k.js → motion-Dv00jw87.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-C251Zo34.js → move-BFz51iHf.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-BDiOORoQ.js → mtc-parser-BK2woIDE.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-DTDA_RHv.js → omit-D2X_Gwoo.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-CqEUTPcT.js → pickAttrs-CLmFyJ1q.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-BOB8QWH3.js → placementArrow-CCzPddQm.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-CZDXP1d9.js → responsiveObserve-hcm_Zlxd.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-DPaRw3E9.js → slide-BxfZEFVp.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-Ba9d7XZ7.js → statusUtils-kzB9pT-t.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-mZKU3lvW.js → styleChecker-CDFd7YCI.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFlexGapSupport-VcauLq0M.js → useFlexGapSupport-Czl7NNhS.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFs-Cal9KRcx.js → useFs-y0SAzirG.js} +1 -1
  151. package/src/assets/web-panel/assets/{usePersonalDataHub-DISoRyQB.js → usePersonalDataHub-BvdPznrA.js} +1 -1
  152. package/src/assets/web-panel/assets/{vnode-B58C6w6B.js → vnode-D8mNbyZ7.js} +1 -1
  153. package/src/assets/web-panel/assets/{zoom-BPlBAMKB.js → zoom-_hun6w9W.js} +1 -1
  154. package/src/assets/web-panel/index.html +1 -1
  155. package/src/command-help-index.json +1 -1
  156. package/src/commands/agent.js +12 -5
  157. package/src/commands/ask.js +96 -21
  158. package/src/commands/complete.js +14 -10
  159. package/src/commands/llm.js +23 -41
  160. package/src/commands/session.js +2 -2
  161. package/src/commands/stream.js +9 -5
  162. package/src/commands/ui.js +30 -2
  163. package/src/data/changelog.json +65 -0
  164. package/src/gateways/ws/llm-chat-protocol.js +131 -40
  165. package/src/gateways/ws/llm-creds.js +29 -79
  166. package/src/gateways/ws/message-dispatcher.js +2 -0
  167. package/src/gateways/ws/ws-agent-handler.js +1 -1
  168. package/src/gateways/ws/ws-session-gateway.js +26 -12
  169. package/src/lib/__tests__/provider-stream.test.js +70 -4
  170. package/src/lib/chat-core.js +186 -24
  171. package/src/lib/cowork-adapter.js +29 -15
  172. package/src/lib/evolution/agent-evolution-runtime-composition.js +10 -7
  173. package/src/lib/evolution/evolution-deployment-config.js +36 -4
  174. package/src/lib/evolution/evolution-deployment-loader.js +117 -4
  175. package/src/lib/evolution/evolution-deployment-profile.js +2 -2
  176. package/src/lib/evolution/evolution-evidence-projector.js +18 -3
  177. package/src/lib/evolution/evolution-file-witness.js +10 -0
  178. package/src/lib/evolution/evolution-ledger.js +237 -28
  179. package/src/lib/evolution/evolution-windows-directory-durability.js +322 -0
  180. package/src/lib/evolution/governed-hub-llm.js +2 -1
  181. package/src/lib/evolution/governed-model-turn.js +4 -1
  182. package/src/lib/evolution/local-ui-evolution-deployment.js +768 -0
  183. package/src/lib/evolution/local-ui-evolution-host.mjs +44 -0
  184. package/src/lib/evolution/local-ui-evolution-runtime.js +624 -0
  185. package/src/lib/evolution/opaque-hex-id.js +28 -0
  186. package/src/lib/hub-llm-client.js +22 -14
  187. package/src/lib/image-input.js +19 -3
  188. package/src/lib/llm-config-defaults.js +19 -30
  189. package/src/lib/llm-connection-config.js +4 -0
  190. package/src/lib/llm-connection-probe.js +5 -1
  191. package/src/lib/llm-connection-resolution.js +94 -0
  192. package/src/lib/llm-providers.js +33 -11
  193. package/src/lib/ollama-base-url.js +14 -0
  194. package/src/lib/provider-protocol.js +104 -0
  195. package/src/lib/provider-stream.js +163 -4
  196. package/src/lib/runnable-provider.js +1 -0
  197. package/src/repl/chat-repl.js +58 -11
  198. package/src/runtime/agent-core.js +26 -43
  199. package/src/runtime/headless-runner.js +11 -4
  200. package/src/runtime/headless-stream.js +10 -4
  201. package/src/runtime/policies/agent-policy.js +17 -11
  202. package/src/assets/web-panel/assets/ChatBubbleRenderer-DOX3Y5EJ.js +0 -1
  203. package/src/assets/web-panel/assets/PdhVaultBrowser-D9McHF9I.js +0 -7
  204. package/src/assets/web-panel/assets/QuickAsk-DWG-A5zB.js +0 -1
  205. package/src/assets/web-panel/assets/devWarning-DYhpRtve.js +0 -1
  206. package/src/assets/web-panel/assets/index-DPJXFIY7.js +0 -1
  207. package/src/assets/web-panel/assets/index-DjBhR5M_.js +0 -1
@@ -37,15 +37,35 @@ import {
37
37
  streamOllama,
38
38
  streamOpenAI,
39
39
  streamAnthropic,
40
+ streamGemini,
41
+ STREAM_STALL_MS,
40
42
  } from "../../lib/chat-core.js";
41
43
  import { BUILT_IN_PROVIDERS } from "../../lib/llm-providers.js";
42
44
  import { resolveLlmCreds } from "./llm-creds.js";
43
45
  import { prepareGovernedModelTurn } from "../../lib/evolution/governed-model-turn.js";
46
+ import { normalizeOllamaBaseUrl } from "../../lib/ollama-base-url.js";
47
+ import { normalizeProviderBaseUrl } from "../../lib/provider-protocol.js";
48
+ import { raceWithAbort } from "../../lib/abort-utils.js";
44
49
 
45
- function normalizeOllamaBaseUrl(value) {
46
- return String(value || "http://localhost:11434")
47
- .replace(/\/v1\/?$/i, "")
48
- .replace(/\/+$/, "");
50
+ const activeChats = new WeakMap();
51
+ const CHAT_SETUP_TIMEOUT_MS = 60_000;
52
+
53
+ /** A request id is scoped to its originating socket, never another client. */
54
+ export function cancelLlmChat(ws, id) {
55
+ activeChats.get(ws)?.get(id)?.abort(new Error("Chat cancelled"));
56
+ }
57
+
58
+ async function waitForChatPhase(operation, controller, phase) {
59
+ const timer = setTimeout(
60
+ () => controller.abort(new Error(`Chat ${phase} timed out after 60s`)),
61
+ CHAT_SETUP_TIMEOUT_MS,
62
+ );
63
+ timer.unref?.();
64
+ try {
65
+ return await raceWithAbort(operation, controller.signal);
66
+ } finally {
67
+ clearTimeout(timer);
68
+ }
49
69
  }
50
70
 
51
71
  function validateMessages(messages) {
@@ -80,7 +100,17 @@ export async function handleLlmChat(server, id, ws, message) {
80
100
  return;
81
101
  }
82
102
 
83
- const creds = resolveLlmCreds(server, message);
103
+ let creds;
104
+ try {
105
+ creds = resolveLlmCreds(server, message);
106
+ } catch (error) {
107
+ send({
108
+ type: `${topic}.result`,
109
+ ok: false,
110
+ error: error?.message || String(error),
111
+ });
112
+ return;
113
+ }
84
114
  if (!creds || !creds.provider) {
85
115
  send({
86
116
  type: `${topic}.result`,
@@ -103,6 +133,7 @@ export async function handleLlmChat(server, id, ws, message) {
103
133
 
104
134
  let accumulator = "";
105
135
  const onToken = (token) => {
136
+ controller.signal.throwIfAborted();
106
137
  if (typeof token !== "string" || token.length === 0) return;
107
138
  accumulator += token;
108
139
  send({
@@ -117,68 +148,126 @@ export async function handleLlmChat(server, id, ws, message) {
117
148
  };
118
149
 
119
150
  const controller = new AbortController();
151
+ let requests = null;
152
+ if (ws && (typeof ws === "object" || typeof ws === "function")) {
153
+ requests = activeChats.get(ws);
154
+ if (!requests) activeChats.set(ws, (requests = new Map()));
155
+ if (requests.has(id)) {
156
+ send({
157
+ type: `${topic}.result`,
158
+ ok: false,
159
+ error: "Chat request is already active",
160
+ });
161
+ return;
162
+ }
163
+ requests.set(id, controller);
164
+ }
120
165
  const disconnect = () =>
121
166
  controller.abort(new Error("Chat connection closed"));
167
+ const onStall = (elapsedMs, timeoutMs) => {
168
+ if (controller.signal.aborted) return;
169
+ send({
170
+ type: `${topic}.status`,
171
+ status: {
172
+ stage: "waiting",
173
+ provider: creds.provider,
174
+ model: creds.model,
175
+ elapsedMs,
176
+ timeoutMs,
177
+ },
178
+ });
179
+ };
122
180
  ws?.once?.("close", disconnect);
123
181
  try {
124
- const turn = await prepareGovernedModelTurn(
125
- server.evolutionCompositionFactory ?? null,
126
- {
182
+ const turn = await waitForChatPhase(
183
+ prepareGovernedModelTurn(server.evolutionCompositionFactory ?? null, {
127
184
  mode: "ws-llm-chat",
128
185
  messages: message.messages,
129
186
  signal: controller.signal,
130
- },
187
+ }),
188
+ controller,
189
+ "initialization",
131
190
  );
132
191
  const streamOptions = {
133
192
  requireCompletion: turn.governed,
134
193
  signal: controller.signal,
135
194
  };
195
+ // Tell the browser the configured deadline even when waiting hints are
196
+ // disabled, so a slow local model gets its full loading budget.
197
+ onStall(0, STREAM_STALL_MS);
136
198
  const baseUrl =
137
199
  creds.provider === "ollama"
138
200
  ? normalizeOllamaBaseUrl(creds.baseUrl)
139
- : creds.baseUrl || "http://localhost:11434";
201
+ : normalizeProviderBaseUrl(creds.baseUrl);
140
202
  if (creds.provider === "ollama") {
141
- await streamOllama(
142
- turn.messages,
143
- creds.model,
144
- baseUrl,
145
- onToken,
146
- onUsage,
147
- undefined,
148
- streamOptions,
203
+ await raceWithAbort(
204
+ streamOllama(
205
+ turn.messages,
206
+ creds.model,
207
+ baseUrl,
208
+ onToken,
209
+ onUsage,
210
+ onStall,
211
+ streamOptions,
212
+ ),
213
+ controller.signal,
149
214
  );
150
215
  } else if (creds.provider === "anthropic") {
151
- const def = BUILT_IN_PROVIDERS.anthropic;
152
- const url = baseUrl !== "http://localhost:11434" ? baseUrl : def.baseUrl;
153
- await streamAnthropic(
154
- turn.messages,
155
- creds.model,
156
- url,
157
- creds.apiKey,
158
- onToken,
159
- onUsage,
160
- undefined,
161
- streamOptions,
216
+ const url = baseUrl;
217
+ await raceWithAbort(
218
+ streamAnthropic(
219
+ turn.messages,
220
+ creds.model,
221
+ url,
222
+ creds.apiKey,
223
+ onToken,
224
+ onUsage,
225
+ onStall,
226
+ streamOptions,
227
+ ),
228
+ controller.signal,
229
+ );
230
+ } else if (creds.provider === "gemini") {
231
+ const url = baseUrl;
232
+ await raceWithAbort(
233
+ streamGemini(
234
+ turn.messages,
235
+ creds.model,
236
+ url,
237
+ creds.apiKey,
238
+ onToken,
239
+ onUsage,
240
+ onStall,
241
+ streamOptions,
242
+ ),
243
+ controller.signal,
162
244
  );
163
245
  } else {
164
246
  // OpenAI-compatible — covers openai / volcengine / deepseek / dashscope /
165
247
  // kimi / minimax / mistral / custom. baseUrl from creds; default-from
166
248
  // BUILT_IN_PROVIDERS already applied above.
167
249
  const def = BUILT_IN_PROVIDERS[creds.provider];
168
- const url = baseUrl !== "http://localhost:11434" ? baseUrl : def?.baseUrl;
169
- await streamOpenAI(
170
- turn.messages,
171
- creds.model,
172
- url,
173
- creds.apiKey,
174
- onToken,
175
- onUsage,
176
- undefined,
177
- streamOptions,
250
+ const url = baseUrl || def?.baseUrl;
251
+ await raceWithAbort(
252
+ streamOpenAI(
253
+ turn.messages,
254
+ creds.model,
255
+ url,
256
+ creds.apiKey,
257
+ onToken,
258
+ onUsage,
259
+ onStall,
260
+ streamOptions,
261
+ ),
262
+ controller.signal,
178
263
  );
179
264
  }
180
265
 
181
- await turn.complete(accumulator);
266
+ await waitForChatPhase(
267
+ turn.complete(accumulator),
268
+ controller,
269
+ "completion",
270
+ );
182
271
  send({
183
272
  type: `${topic}.result`,
184
273
  ok: true,
@@ -197,6 +286,8 @@ export async function handleLlmChat(server, id, ws, message) {
197
286
  error: err?.message || String(err),
198
287
  });
199
288
  } finally {
289
+ requests?.delete(id);
290
+ if (requests?.size === 0) activeChats.delete(ws);
200
291
  ws?.removeListener?.("close", disconnect);
201
292
  }
202
293
  }
@@ -1,28 +1,11 @@
1
1
  /**
2
- * Shared LLM credential resolver for WS handlers.
2
+ * Shared LLM connection resolver for WS handlers.
3
3
  *
4
- * Used by both `llm.chat` (single-shot streaming) and `chat.intent.*`
5
- * (intent-understanding flow). Encapsulates the precedence rules so adding
6
- * a third consumer doesn't fork them again.
7
- *
8
- * Resolution order (first non-null wins):
9
- * 1. Explicit `message.options.{provider,model,baseUrl,apiKey}` from client.
10
- * 2. WS session creds via `server.sessionManager.getSession(sessionId)`.
11
- * 3. Current saved `config.llm` (refreshed per request by the WS server).
12
- * 4. Process env — first BUILT_IN_PROVIDERS entry whose apiKeyEnv is set.
13
- *
14
- * Fall-throughs:
15
- * - Provider with `free: true` (ollama) doesn't need apiKey; a session
16
- * entry with provider=ollama and no apiKey is valid.
17
- * - For non-ollama providers, missing apiKey is a fatal — the caller
18
- * should send a clean error frame instead of hanging on a 401.
19
- * - When no source has any creds, returns null. Caller decides whether
20
- * to fail-fast (llm.chat) or yield "LLM not configured" (chat-intent).
21
- *
22
- * @returns {{provider, model, baseUrl, apiKey} | null}
4
+ * Provider, model, endpoint, and credential are resolved together so a
5
+ * provider override cannot inherit a model or key from another endpoint.
23
6
  */
24
-
25
7
  import { BUILT_IN_PROVIDERS } from "../../lib/llm-providers.js";
8
+ import { resolveLlmConnection } from "../../lib/llm-connection-resolution.js";
26
9
 
27
10
  const ENV_PREFERRED_ORDER = [
28
11
  "volcengine",
@@ -38,84 +21,51 @@ const ENV_PREFERRED_ORDER = [
38
21
 
39
22
  export function resolveLlmCreds(server, message) {
40
23
  const opts = message?.options || {};
41
- const optProvider = opts.provider;
42
- const optModel = opts.model;
43
- const optBaseUrl = opts.baseUrl;
44
- const optApiKey = opts.apiKey;
45
24
  let cfgLlm = {};
46
25
  if (typeof server?._loadLlmConfig === "function") {
47
26
  try {
48
27
  const configured = server._loadLlmConfig();
49
28
  cfgLlm = configured?.llm || configured || {};
50
29
  } catch {
51
- // If the saved config cannot be refreshed, continue with explicit
52
- // options, session credentials, or environment credentials. The
53
- // session manager snapshot may contain merged defaults rather than
54
- // values the user actually saved.
30
+ // A stale config snapshot can still be bypassed with session or env data.
55
31
  }
56
32
  } else {
57
33
  cfgLlm = server?.sessionManager?.config?.llm || {};
58
34
  }
59
35
 
60
- const buildCreds = (provider, inherited = {}) => {
61
- const def = BUILT_IN_PROVIDERS[provider];
62
- const sameConfiguredProvider = provider === cfgLlm.provider;
63
- return {
64
- provider,
65
- model:
66
- optModel ||
67
- inherited.model ||
68
- (sameConfiguredProvider ? cfgLlm.model : null) ||
69
- def?.models?.[0] ||
70
- null,
71
- baseUrl:
72
- optBaseUrl ||
73
- inherited.baseUrl ||
74
- (sameConfiguredProvider ? cfgLlm.baseUrl : null) ||
75
- def?.baseUrl ||
76
- (provider === "ollama" ? "http://localhost:11434" : null),
77
- apiKey:
78
- optApiKey ||
79
- inherited.apiKey ||
80
- (sameConfiguredProvider ? cfgLlm.apiKey : null) ||
81
- (def?.apiKeyEnv ? process.env[def.apiKeyEnv] : null),
82
- };
83
- };
84
-
85
- if (optProvider) {
86
- return buildCreds(optProvider);
87
- }
88
-
36
+ let session = null;
89
37
  const sessionId = message?.sessionId;
90
38
  if (sessionId && server?.sessionManager?.getSession) {
91
- let session;
92
39
  try {
93
40
  session = server.sessionManager.getSession(sessionId);
94
41
  } catch {
95
42
  session = null;
96
43
  }
97
- if (session?.provider) {
98
- // Don't default baseUrl to ollama — that breaks every cloud provider
99
- // when the session was created without an explicit baseUrl. Fall back
100
- // to the provider's own default endpoint instead.
101
- return buildCreds(session.provider, {
102
- model: session.model,
103
- baseUrl: session.baseUrl,
104
- apiKey: session.apiKey,
105
- });
106
- }
107
44
  }
108
45
 
109
- if (cfgLlm.provider) {
110
- return buildCreds(cfgLlm.provider);
111
- }
112
-
113
- for (const name of ENV_PREFERRED_ORDER) {
114
- const def = BUILT_IN_PROVIDERS[name];
115
- if (def?.apiKeyEnv && process.env[def.apiKeyEnv]) {
116
- return buildCreds(name);
117
- }
46
+ const envProvider = ENV_PREFERRED_ORDER.find((name) => {
47
+ const envName = BUILT_IN_PROVIDERS[name]?.apiKeyEnv;
48
+ return envName && process.env[envName];
49
+ });
50
+ const defaultProvider =
51
+ opts.provider || session?.provider || cfgLlm.provider || envProvider || "ollama";
52
+ if (!opts.provider && !session?.provider && !cfgLlm.provider && !envProvider) {
53
+ return null;
118
54
  }
119
55
 
120
- return null;
56
+ return resolveLlmConnection(
57
+ opts,
58
+ [
59
+ session
60
+ ? {
61
+ provider: session.provider,
62
+ model: session.model,
63
+ baseUrl: session.baseUrl,
64
+ apiKey: session.apiKey,
65
+ }
66
+ : null,
67
+ cfgLlm,
68
+ ],
69
+ { defaultProvider },
70
+ );
121
71
  }
@@ -30,6 +30,7 @@ import {
30
30
  handleRemoteSessionTerminal,
31
31
  } from "./remote-session-protocol.js";
32
32
  import { RemoteCommandLedger } from "../../harness/remote-command-ledger.js";
33
+ import { cancelLlmChat } from "./llm-chat-protocol.js";
33
34
 
34
35
  /**
35
36
  * Reconnect-safe command execution. When a message carries a stable
@@ -293,6 +294,7 @@ export function createWsMessageDispatcher(server) {
293
294
  "chat.intent.classify-followup": () =>
294
295
  server._handleChatIntentClassifyFollowup(id, ws, message),
295
296
  "llm.chat": () => server._handleLlmChat(id, ws, message),
297
+ "llm.chat.cancel": () => cancelLlmChat(ws, id),
296
298
  };
297
299
 
298
300
  // Static core routes take precedence and are dispatched directly. The
@@ -1530,7 +1530,7 @@ export class WSAgentHandler {
1530
1530
  }),
1531
1531
  provider: session.provider,
1532
1532
  model: activeModel,
1533
- baseUrl: session.baseUrl || "http://localhost:11434",
1533
+ baseUrl: session.baseUrl,
1534
1534
  apiKey: session.apiKey,
1535
1535
  contextEngine: session.contextEngine,
1536
1536
  hookDb: this.db,
@@ -36,6 +36,7 @@ import {
36
36
  deleteSession as dbDeleteSession,
37
37
  } from "../../lib/session-manager.js";
38
38
  import { buildSystemPrompt } from "../../runtime/agent-core.js";
39
+ import { resolveLlmConnection } from "../../lib/llm-connection-resolution.js";
39
40
  import { createHostToolPolicyAuthority } from "../../lib/host-tool-policy-authority.js";
40
41
  import { SubAgentRegistry } from "../../lib/sub-agent-registry.js";
41
42
  import {
@@ -518,14 +519,13 @@ export class WSSessionManager {
518
519
  ? this._hooksV2HostWorkspaceRoot
519
520
  : null;
520
521
  const cfgLlm = this.config?.llm || {};
521
- const provider = options.provider || cfgLlm.provider || "ollama";
522
- const model =
523
- options.model ||
524
- cfgLlm.model ||
525
- (provider === "ollama" ? "qwen2.5:7b" : null);
526
- const baseUrl =
527
- options.baseUrl || cfgLlm.baseUrl || "http://localhost:11434";
528
- const apiKey = options.apiKey || cfgLlm.apiKey || null;
522
+ const connection = resolveLlmConnection(options, [cfgLlm], {
523
+ defaultModel:
524
+ (options.provider || cfgLlm.provider || "ollama") === "ollama"
525
+ ? "qwen2.5:7b"
526
+ : null,
527
+ });
528
+ const { provider, model, baseUrl, apiKey } = connection;
529
529
  const worktreeIsolationRequested = options.worktreeIsolation === true;
530
530
  const enabledToolNames = this._normalizeEnabledToolNames(
531
531
  options.enabledToolNames,
@@ -839,15 +839,29 @@ export class WSSessionManager {
839
839
  // Non-critical
840
840
  }
841
841
 
842
+ const connection = resolveLlmConnection(
843
+ {
844
+ provider: dbSession.provider || "ollama",
845
+ model: dbSession.model || null,
846
+ baseUrl: metadata.baseUrl || null,
847
+ },
848
+ [this.config?.llm || {}],
849
+ {
850
+ defaultModel:
851
+ (dbSession.provider || "ollama") === "ollama"
852
+ ? "qwen2.5:7b"
853
+ : null,
854
+ },
855
+ );
842
856
  const session = {
843
857
  id: dbSession.id,
844
858
  type: metadata.sessionType || "agent",
845
859
  status: "active",
846
860
  messages,
847
- provider: dbSession.provider || "ollama",
848
- model: dbSession.model || null,
849
- apiKey: null,
850
- baseUrl: metadata.baseUrl || "http://localhost:11434",
861
+ provider: connection.provider,
862
+ model: connection.model,
863
+ apiKey: connection.apiKey,
864
+ baseUrl: connection.baseUrl,
851
865
  mcpClient: this.mcpClient,
852
866
  enabledToolNames: this._normalizeEnabledToolNames(
853
867
  metadata.enabledToolNames,
@@ -4,10 +4,9 @@
4
4
  * provider-stream tests (previously untested) — the token-stream parsers behind
5
5
  * `cc stream` and the Hosted Session stream.run route. Pins the bug-prone
6
6
  * streaming parse logic: Ollama NDJSON (yield response, stop on done, line
7
- * buffering across chunk boundaries, skip malformed) and OpenAI-style SSE
8
- * (yield delta.content, stop on [DONE], skip non-data/empty), error on non-ok,
9
- * plus buildProviderSource routing (ollama defaults / known-provider endpoint +
10
- * Authorization / unsupported / missing-key). fetch is stubbed; no network.
7
+ * buffering across chunk boundaries, skip malformed), OpenAI-style SSE, and
8
+ * native Anthropic/Gemini SSE. Also pins protocol routing, completion markers,
9
+ * authentication headers, and URL normalization. fetch is stubbed; no network.
11
10
  */
12
11
 
13
12
  import { describe, it, expect, afterEach, vi } from "vitest";
@@ -173,6 +172,73 @@ describe("buildProviderSource", () => {
173
172
  );
174
173
  });
175
174
 
175
+ it("normalizes a trailing slash on OpenAI-compatible provider bases", async () => {
176
+ const fetchMock = mockFetch(["data: [DONE]\n"]);
177
+ vi.stubGlobal("fetch", fetchMock);
178
+ await collect(
179
+ buildProviderSource("deepseek", {
180
+ baseUrl: "https://api.deepseek.com/v1/",
181
+ apiKey: "sk-x",
182
+ prompt: "hi",
183
+ }),
184
+ );
185
+ expect(fetchMock.mock.calls[0][0]).toBe(
186
+ "https://api.deepseek.com/v1/chat/completions",
187
+ );
188
+ });
189
+
190
+ it("routes Gemini through its native SSE API without putting the key in the URL", async () => {
191
+ const fetchMock = mockFetch([
192
+ 'data: {"candidates":[{"content":{"parts":[{"thought":true,"text":"private"},{"text":"hello"}]}}]}\n',
193
+ 'data: {"candidates":[{"finishReason":"STOP"}]}\n',
194
+ ]);
195
+ vi.stubGlobal("fetch", fetchMock);
196
+ const out = await collect(
197
+ buildProviderSource("gemini", {
198
+ apiKey: "secret",
199
+ model: "gemini-2.0-flash",
200
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/",
201
+ prompt: "hello",
202
+ requireCompletion: true,
203
+ }),
204
+ );
205
+ const [url, options] = fetchMock.mock.calls[0];
206
+ expect(url).toBe(
207
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse",
208
+ );
209
+ expect(url).not.toContain("secret");
210
+ expect(options.headers["x-goog-api-key"]).toBe("secret");
211
+ expect(out).toEqual(["hello"]);
212
+ });
213
+
214
+ it("routes Anthropic through native Messages SSE with a trimmed base URL", async () => {
215
+ const fetchMock = mockFetch([
216
+ 'data: {"type":"content_block_delta","delta":{"text":"hello"}}\n',
217
+ 'data: {"type":"message_stop"}\n',
218
+ ]);
219
+ vi.stubGlobal("fetch", fetchMock);
220
+ const out = await collect(
221
+ buildProviderSource("anthropic", {
222
+ apiKey: "secret",
223
+ model: "claude-haiku-4-5-20251001",
224
+ baseUrl: "https://api.anthropic.com/v1/",
225
+ messages: [
226
+ { role: "system", content: "be concise" },
227
+ { role: "user", content: "hello" },
228
+ ],
229
+ requireCompletion: true,
230
+ }),
231
+ );
232
+ const [url, options] = fetchMock.mock.calls[0];
233
+ expect(url).toBe("https://api.anthropic.com/v1/messages");
234
+ expect(options.headers["x-api-key"]).toBe("secret");
235
+ expect(JSON.parse(options.body)).toMatchObject({
236
+ system: "be concise",
237
+ messages: [{ role: "user", content: "hello" }],
238
+ });
239
+ expect(out).toEqual(["hello"]);
240
+ });
241
+
176
242
  it("throws for an unsupported provider", () => {
177
243
  expect(() => buildProviderSource("nope", { prompt: "x" })).toThrow(
178
244
  /Unsupported provider/,