chainlesschain 0.162.125 → 0.162.126

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 (190) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-DcqNhZhA.js → AIOps-BrTOcqIJ.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BigtmPoq.js → ActionButton-na9cocmf.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-1J_X_HF4.js → Analytics-BtDygkpt.js} +2 -2
  5. package/src/assets/web-panel/assets/{AppLayout-DlcDG_a_.js → AppLayout-D_t5CS18.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-Do-y8_3w.js → Audit-BYMZccCy.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-S2Df-50Y.js → Backup-D2qDfAdL.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-D5kgWJfk.js → BaseInput-kj456Ju9.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-aUcp3kN1.js → Chat-BY0A0ZLE.js} +5 -5
  10. package/src/assets/web-panel/assets/ChatBubbleRenderer-D1VvrOPQ.js +1 -0
  11. package/src/assets/web-panel/assets/{Checkbox-DTg1U7Xs.js → Checkbox-DiBhtXsh.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-YO9ZZ4Ky.js → Codegen-B1ER0L0j.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-BHonE9fU.js → Col-CjLmza7D.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DA2AlEFh.js → Community-BNId05U7.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DULxLRNg.js → Compact-BuuMj7K1.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-BZqfuuZL.js → Compliance-DUuDycaJ.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-CJe3vyMS.js → Cowork-BKHiF6uL.js} +2 -2
  18. package/src/assets/web-panel/assets/{Cron-CeV8AtRu.js → Cron-C7p-lHnC.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-CAg66zS5.js → Crosschain-BvSOx8a6.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-Dtup5JZm.js → DID-D1k9jgHv.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-DAR_8PNy.js → Dashboard-B9mCCnqY.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-K5bTaCYN.js → Dropdown-ge9UHSXF.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-C890uErx.js → EmailListRenderer-TZO7ppXi.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-B-Tj_8yM.js → FamilyGuardDashboard-DkhLJmzx.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-C6WZ98ni.js → Federation-JLuA_8zp.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-D-LTfGWd.js → FormItemContext-C4w-rzNg.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-C80DK4W7.js → GenericCardRenderer-Btns4sV9.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-Cjvvv3zW.js → Git-BheXMjzC.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-C0BND77H.js → Governance-D9rLlatH.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-BL9kTtSu.js → Inference-DiklIMcd.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-CnVTBmJf.js → KnowledgeGraph-C70EWL03.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-CbCbg7K6.js → Logs-Zt_MLI10.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-CG5On-fH.js → Marketplace-suQxES99.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-AYYDZZK7.js → McpTools-CclpCDpY.js} +5 -5
  35. package/src/assets/web-panel/assets/{Memory-tMWbMDQq.js → Memory-B2lHOUwF.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-BAOsf4wB.js → MobileBridge-BMXKoDAD.js} +1 -1
  37. package/src/assets/web-panel/assets/{MobileProjects-BT-2komQ.js → MobileProjects-a6mWVdBV.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-BFwfC63n.js → Mtc-BFpM4Fkj.js} +5 -5
  39. package/src/assets/web-panel/assets/{MtcAudit-DYG3CWdH.js → MtcAudit-DDQkxkbS.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-KJwd0yWT.js → Multisig-Crkd_zKQ.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-BmL5dNWm.js → NLProgramming-Dpk5An7B.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-DmkaVaMc.js → Notes-BrFnOMNz.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-tmVQszDZ.js → NotificationSettings-CM0iApFM.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-BtrR7anf.js → OrderTableRenderer-XXjeqkdz.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-pppktQyW.js → Organization-CHFW_38T.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-D3lBoQDA.js → Overflow-C-vl3EjV.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-BgE2qGlZ.js → P2P-BT7Slavq.js} +2 -2
  48. package/src/assets/web-panel/assets/PdhVaultBrowser-DRUaULap.js +7 -0
  49. package/src/assets/web-panel/assets/{Permissions-BR8no2Xe.js → Permissions-DkhOAvMz.js} +3 -3
  50. package/src/assets/web-panel/assets/{PersonalDataHub-DabbyQEF.js → PersonalDataHub-Dl7dSOvV.js} +2 -2
  51. package/src/assets/web-panel/assets/{Pipeline-n4sNpEz8.js → Pipeline-B3nVI4p6.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-CGNp4n8M.js → Privacy-Dv1SXx1Q.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-CAVA5VsX.js → ProjectInit-DrAhVi5p.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-GCgkfM7A.js → ProjectSettings-h-ggCYN_.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-BYK17p52.js → Projects-CM91hYdr.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-DpUT5W-d.js → Providers-rfkZel3N.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-ZJxTb7vi.js → QuickAsk-XepZB7h_.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-CIzFRDk1.js → Recommend-DzQWsh1a.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-lQ_WDNZn.js → Reputation-CDeaal0j.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-1MEtrgtF.js → Row-BN4aKhFH.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-SSgcPPl4.js → RssFeed-INP6VYAA.js} +2 -2
  62. package/src/assets/web-panel/assets/{Search-CcTMOGNY.js → Search-ChPbwatu.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-BA1KMaDx.js → Security-C97ZCY8N.js} +3 -3
  64. package/src/assets/web-panel/assets/{Services-_aj7czZn.js → Services-Cbtl2Ajs.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-BWgg_0Zr.js → Skeleton-B13sFxBQ.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-C2ZIziYM.js → Skills-vI9zSIKX.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-CYxp4axY.js → Sla-Cr3oBH39.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-C7Csp1jV.js → SpeechSettings-BwemqPPV.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-svGeswiw.js → SyncSettings-CLA78P-Z.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-ClISj6oK.js → Tasks-DBjQ_DOM.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-CmO-Fp7r.js → Templates-BkSLDkBs.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-a3Ac3lxz.js → Tenant-Dn0nSlib.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-DyJIRh81.js → Terminal-C8f4boru.js} +2 -2
  74. package/src/assets/web-panel/assets/{TimelineRenderer-acXS5N5h.js → TimelineRenderer-DW_rMtJR.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-DJW0KqV4.js → Tokens-BdffrKYV.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-0HeTi4r6.js → Trigger-CX4CUg-Q.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-aFym34eo.js → Trust-Cq9Bl7Od.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-CYhszHJv.js → UkeySign-BQy18_VY.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-BbJxPF9X.js → VideoEditing-h5hzK7Vw.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-DDthEwiu.js → Wallet-BLotT3gw.js} +2 -2
  81. package/src/assets/web-panel/assets/{WebAuthn-DQ6McYPg.js → WebAuthn-8cCANYLE.js} +2 -2
  82. package/src/assets/web-panel/assets/{WorkflowEditor-CnF8zRsi.js → WorkflowEditor-Db6NwtAv.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-Dzkx2gTP.js → chat-BPk1LbpL.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-35T51Oo5.js → colors-BnNal71p.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-TtzNrlu1.js → compact-item-W2VC0tcy.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-Y1LkWrYb.js → createContext-DceVsgnZ.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning--rwFDBhx.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-BOYw1BgS.js → hasIn-BEq6EDcp.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-BtlJWaSP.js → index-5gIBUFBn.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-CLqC2sRT.js → index-APCHxOkM.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-C7Wt5feN.js → index-B2dMhwZi.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-CqhwG1ox.js → index-B8cPjrEH.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BxWUEFKy.js → index-BDC5HsxZ.js} +1 -1
  94. package/src/assets/web-panel/assets/index-BPFuPGKi.js +1 -0
  95. package/src/assets/web-panel/assets/{index-NM9Oke2k.js → index-BUkDcIwJ.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BWgNn9As.js → index-BWcCyH6-.js} +3 -3
  97. package/src/assets/web-panel/assets/{index-CVrUs6rf.js → index-BgZikQoh.js} +1 -1
  98. package/src/assets/web-panel/assets/index-Bwjus13Y.js +1 -0
  99. package/src/assets/web-panel/assets/{index-D6tG4B25.js → index-BzSzRWsw.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DjeUZxE5.js → index-C1K9CsGr.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DmkG479D.js → index-CBZVISK6.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Bw1iOGhM.js → index-CEJN-R2L.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-G4ClSmJc.js → index-CG5dGC9E.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-D47robkX.js → index-CMGLpstm.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BKRlWHUp.js → index-CWrGCndd.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DhMSOd_2.js → index-CbcvfpCV.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Dx0hIfC-.js → index-Ceqv4lI2.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CdyFg4FI.js → index-CsT6frT8.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-B7aKAZzS.js → index-D8PQKsDT.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-8jffdb6b.js → index-DGncdA-j.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-XIuZV9by.js → index-DHanQHO0.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Dhfw-BXs.js → index-DaUBk28E.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-hyqEKkBT.js → index-Di2J4b4V.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-DAizH273.js → index-DtuLvWZN.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CPGNc2tF.js → index-DvS1J8xc.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-KgXrlfNF.js → index-DxePJdwP.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DFhlelzN.js → index-Et4XvL49.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-zF77jnI2.js → index-GlFxgcj4.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-Cs82BHAj.js → index-HV119Oui.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Dof7lWA_.js → index-InGW87pj.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Bs69SI96.js → index-P1-s8JR7.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D4BgCBa3.js → index-SdABCNoi.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BSpFe6j5.js → index-WTAZbN-c.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D7dCBw_M.js → index-_2I-KzOj.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BGPEaeB1.js → index-m1sVaWVU.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CZ16GlOs.js → index-s37wwyeN.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-D2od7Bgx.js → index-tyWABqp9.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-DD5y5msp.js → initDefaultProps-DzrCDb3j.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-BuoutMia.js → motion-BQERVnE3.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-DkpjqOXg.js → move-I9ACA5XJ.js} +1 -1
  131. package/src/assets/web-panel/assets/{mtc-parser-BmGJD-v-.js → mtc-parser-Dd2Pw-tr.js} +1 -1
  132. package/src/assets/web-panel/assets/{omit-DftUE0Sz.js → omit-DwzYRzWV.js} +1 -1
  133. package/src/assets/web-panel/assets/{pickAttrs-DDRb9FIu.js → pickAttrs-BSGULyIL.js} +1 -1
  134. package/src/assets/web-panel/assets/{placementArrow-DPgtxOOD.js → placementArrow-tSYYjAts.js} +1 -1
  135. package/src/assets/web-panel/assets/{responsiveObserve-BQFRzFeZ.js → responsiveObserve-Dy2lqikT.js} +1 -1
  136. package/src/assets/web-panel/assets/{slide-DQ4lmUSz.js → slide-RaB4Uq0c.js} +1 -1
  137. package/src/assets/web-panel/assets/{statusUtils-C1TR4ZiQ.js → statusUtils-CBpC_wZg.js} +1 -1
  138. package/src/assets/web-panel/assets/{styleChecker-B-Suj978.js → styleChecker-BEKwPWt5.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFlexGapSupport-Q7bDZ3EI.js → useFlexGapSupport-C4CnYJH1.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFs-DHjRLyQH.js → useFs-n24jutw5.js} +1 -1
  141. package/src/assets/web-panel/assets/{usePersonalDataHub-lQOvCvCr.js → usePersonalDataHub-DMgS8F6G.js} +1 -1
  142. package/src/assets/web-panel/assets/{vnode-Ie0g4-p3.js → vnode-BSp2KYcj.js} +1 -1
  143. package/src/assets/web-panel/assets/{zoom-CHP0YEoH.js → zoom-7UfQtTPh.js} +1 -1
  144. package/src/assets/web-panel/index.html +1 -1
  145. package/src/commands/agent.js +41 -2
  146. package/src/commands/compliance.js +3 -1
  147. package/src/commands/review.js +47 -17
  148. package/src/commands/session.js +11 -2
  149. package/src/gateways/ws/message-dispatcher.js +145 -165
  150. package/src/gateways/ws/ws-session-gateway.js +39 -5
  151. package/src/harness/jsonl-session-store.js +39 -12
  152. package/src/harness/worktree-isolator.js +5 -0
  153. package/src/lib/agent-core.js +1 -0
  154. package/src/lib/audit-logger.js +3 -1
  155. package/src/lib/chat-core.js +5 -2
  156. package/src/lib/chat-intent-service.js +68 -21
  157. package/src/lib/config-manager.js +25 -1
  158. package/src/lib/credential-guard.js +43 -1
  159. package/src/lib/git-integration.js +5 -0
  160. package/src/lib/interaction-adapter.js +32 -11
  161. package/src/lib/jsonl-session-store.js +1 -0
  162. package/src/lib/llm-config-defaults.js +37 -0
  163. package/src/lib/mcp-oauth.js +76 -10
  164. package/src/lib/permission-engine.js +4 -1
  165. package/src/lib/pipeline-orchestrator.js +20 -2
  166. package/src/lib/project-instructions.js +13 -0
  167. package/src/lib/repl-denials.js +137 -0
  168. package/src/lib/safe-json.js +22 -0
  169. package/src/lib/sandbox-v2.js +7 -6
  170. package/src/lib/search-command.js +65 -0
  171. package/src/lib/settings-hooks.cjs +133 -0
  172. package/src/lib/settings-loader.cjs +16 -7
  173. package/src/lib/social-graph.js +3 -1
  174. package/src/lib/stream-retry.js +27 -0
  175. package/src/lib/turn-context.js +15 -5
  176. package/src/repl/agent-repl.js +81 -8
  177. package/src/runtime/__tests__/message-roles.test.js +36 -3
  178. package/src/runtime/agent-core.js +175 -50
  179. package/src/runtime/coding-agent-contract-shared.cjs +9 -2
  180. package/src/runtime/coding-agent-shell-policy.cjs +23 -2
  181. package/src/runtime/file-ref-expander.js +51 -7
  182. package/src/runtime/headless-runner.js +83 -2
  183. package/src/runtime/headless-stream.js +69 -3
  184. package/src/runtime/mcp-config.js +42 -1
  185. package/src/runtime/message-roles.js +14 -1
  186. package/src/assets/web-panel/assets/ChatBubbleRenderer-Pg7dIsiE.js +0 -1
  187. package/src/assets/web-panel/assets/PdhVaultBrowser-D26Bz5gS.js +0 -7
  188. package/src/assets/web-panel/assets/devWarning-D_lwLZ6O.js +0 -1
  189. package/src/assets/web-panel/assets/index-CEwuCM44.js +0 -1
  190. package/src/assets/web-panel/assets/index-DzzgU4IU.js +0 -1
@@ -123,6 +123,33 @@ export class WSSessionManager {
123
123
  this.maxPendingPatches = Number.isFinite(options.maxPendingPatches)
124
124
  ? options.maxPendingPatches
125
125
  : 50;
126
+ // The entry-count caps above bound how MANY patches are kept, but a SINGLE
127
+ // proposed patch (a client-reachable `patch-propose`) could still carry an
128
+ // unbounded number of files, each with unbounded before/after/diff content
129
+ // — all held in memory AND serialized to disk by _persistSessionState. One
130
+ // file with a multi-GB diff string is enough to OOM. Bound a single patch's
131
+ // payload: cap the file count and truncate each content field.
132
+ this.maxPatchFiles = Number.isFinite(options.maxPatchFiles)
133
+ ? options.maxPatchFiles
134
+ : 200;
135
+ this.maxPatchContentChars = Number.isFinite(options.maxPatchContentChars)
136
+ ? options.maxPatchContentChars
137
+ : 512 * 1024; // 512 KB per before/after/diff field
138
+ }
139
+
140
+ /**
141
+ * Truncate a patch content field (before/after/diff) to maxPatchContentChars,
142
+ * appending a visible marker. Preserves the null-vs-string distinction (a null
143
+ * field stays null — it signals create/delete ops). Pure.
144
+ */
145
+ _capPatchContent(value) {
146
+ if (value == null) return null;
147
+ const s = String(value);
148
+ if (s.length <= this.maxPatchContentChars) return s;
149
+ return (
150
+ s.slice(0, this.maxPatchContentChars) +
151
+ `\n…[truncated for storage: field was ${s.length} chars]`
152
+ );
126
153
  }
127
154
 
128
155
  _normalizeEnabledToolNames(enabledToolNames) {
@@ -854,8 +881,13 @@ export class WSSessionManager {
854
881
  const session = this.sessions.get(sessionId);
855
882
  if (!session) return null;
856
883
 
857
- const files = Array.isArray(payload.files) ? payload.files : [];
858
- if (files.length === 0) return null;
884
+ const rawFiles = Array.isArray(payload.files) ? payload.files : [];
885
+ if (rawFiles.length === 0) return null;
886
+ // Cap the per-patch file count; a proposal touching more than this is
887
+ // pathological for review. Excess files are dropped (count recorded below)
888
+ // so one message can't pin unbounded memory / disk.
889
+ const files = rawFiles.slice(0, this.maxPatchFiles);
890
+ const droppedFiles = rawFiles.length - files.length;
859
891
 
860
892
  const patchId = `patch-${this._generateId()}`;
861
893
  const now = new Date().toISOString();
@@ -866,9 +898,9 @@ export class WSSessionManager {
866
898
  index,
867
899
  path: file.path || `unknown-${index}`,
868
900
  op,
869
- before: file.before == null ? null : String(file.before),
870
- after: file.after == null ? null : String(file.after),
871
- diff: file.diff == null ? null : String(file.diff),
901
+ before: this._capPatchContent(file.before),
902
+ after: this._capPatchContent(file.after),
903
+ diff: this._capPatchContent(file.diff),
872
904
  stats,
873
905
  };
874
906
  });
@@ -895,6 +927,8 @@ export class WSSessionManager {
895
927
  fileCount: normalizedFiles.length,
896
928
  added: totalStats.added,
897
929
  removed: totalStats.removed,
930
+ // Surfaced so the reviewer/UI knows the proposal was capped for storage.
931
+ ...(droppedFiles > 0 ? { droppedFiles } : {}),
898
932
  },
899
933
  };
900
934
 
@@ -113,20 +113,36 @@ export function readEvents(sessionId) {
113
113
  return events;
114
114
  }
115
115
 
116
+ /**
117
+ * A replayable chat message must be a `{ role, content }` object — guard
118
+ * against a corrupt / partially-written / hand-edited event whose `data` is
119
+ * missing, null, or not a message (it would otherwise inject `undefined` into
120
+ * the resumed history and break the next LLM request).
121
+ */
122
+ function isReplayableMessage(m) {
123
+ return Boolean(m) && typeof m === "object" && typeof m.role === "string";
124
+ }
125
+
116
126
  export function rebuildMessages(sessionId) {
117
127
  const events = readEvents(sessionId);
118
128
  const messages = [];
119
129
  let lastCompactIndex = -1;
120
130
 
131
+ // A `compact` event carries the pre-compaction snapshot in data.messages. A
132
+ // malformed compact line (type present but data null / messages not an array)
133
+ // must not crash the resume — skip it and look further back.
121
134
  for (let i = events.length - 1; i >= 0; i--) {
122
- if (events[i].type === "compact" && events[i].data.messages) {
135
+ const e = events[i];
136
+ if (e && e.type === "compact" && Array.isArray(e.data?.messages)) {
123
137
  lastCompactIndex = i;
124
138
  break;
125
139
  }
126
140
  }
127
141
 
128
- if (lastCompactIndex >= 0 && events[lastCompactIndex].data.messages) {
129
- messages.push(...events[lastCompactIndex].data.messages);
142
+ if (lastCompactIndex >= 0) {
143
+ for (const m of events[lastCompactIndex].data.messages) {
144
+ if (isReplayableMessage(m)) messages.push(m);
145
+ }
130
146
  }
131
147
 
132
148
  const startIndex = lastCompactIndex >= 0 ? lastCompactIndex + 1 : 0;
@@ -134,9 +150,11 @@ export function rebuildMessages(sessionId) {
134
150
  for (let i = startIndex; i < events.length; i++) {
135
151
  const event = events[i];
136
152
  if (
137
- event.type === "user_message" ||
138
- event.type === "assistant_message" ||
139
- event.type === "system"
153
+ event &&
154
+ (event.type === "user_message" ||
155
+ event.type === "assistant_message" ||
156
+ event.type === "system") &&
157
+ isReplayableMessage(event.data)
140
158
  ) {
141
159
  messages.push(event.data);
142
160
  }
@@ -145,6 +163,19 @@ export function rebuildMessages(sessionId) {
145
163
  return messages;
146
164
  }
147
165
 
166
+ /** ISO string for a numeric ms timestamp, or "" when missing / non-finite /
167
+ * invalid — `new Date(undefined).toISOString()` (and `new Date("garbage")`)
168
+ * throw "Invalid time value", and one corrupt event must not crash a whole
169
+ * `cc session list` / `cc session search`. Exported so command-layer readers of
170
+ * the same (hand-editable) JSONL share the guard. */
171
+ export function toIsoSafe(ts) {
172
+ if (ts == null) return ""; // null/undefined → "" (Number(null) is 0 = epoch)
173
+ const n = Number(ts);
174
+ if (!Number.isFinite(n)) return "";
175
+ const d = new Date(n);
176
+ return Number.isNaN(d.getTime()) ? "" : d.toISOString();
177
+ }
178
+
148
179
  export function listJsonlSessions(options = {}) {
149
180
  const dir = getSessionsDir();
150
181
  if (!existsSync(dir)) return [];
@@ -167,12 +198,8 @@ export function listJsonlSessions(options = {}) {
167
198
  provider: startEvent?.data?.provider || "",
168
199
  model: startEvent?.data?.model || "",
169
200
  message_count: messageCount,
170
- created_at: startEvent
171
- ? new Date(startEvent.timestamp).toISOString()
172
- : "",
173
- updated_at: lastEvent
174
- ? new Date(lastEvent.timestamp).toISOString()
175
- : "",
201
+ created_at: toIsoSafe(startEvent?.timestamp),
202
+ updated_at: toIsoSafe(lastEvent?.timestamp),
176
203
  _lastTs: lastEvent?.timestamp || 0,
177
204
  _eventCount: events.length,
178
205
  };
@@ -550,6 +550,11 @@ function _hasUncommittedChanges(worktreePath) {
550
550
  const output = execSync("git status --porcelain", {
551
551
  cwd: worktreePath,
552
552
  encoding: "utf-8",
553
+ // A worktree with many dirty files produces >1 MB of porcelain output and
554
+ // would ENOBUFS on the default cap — the catch below then reports "clean"
555
+ // (fail-OPEN), which could let a caller discard a worktree full of
556
+ // uncommitted work. Give it the same 64 MB headroom as gitExec.
557
+ maxBuffer: 64 * 1024 * 1024,
553
558
  });
554
559
  return output.trim().length > 0;
555
560
  } catch (_e) {
@@ -29,6 +29,7 @@ export {
29
29
  killBackgroundShellTask,
30
30
  killAllBackgroundShellTasks,
31
31
  reapIdleBackgroundShellTasks,
32
+ _ingestSearchOutput,
32
33
  writeFileVerified,
33
34
  formatProviderHttpError,
34
35
  _accumulateOllamaStream,
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import crypto from "crypto";
7
+ import { safeJsonParse } from "./safe-json.js";
7
8
 
8
9
  /**
9
10
  * Event types for audit logging.
@@ -232,7 +233,8 @@ export function queryLogs(db, filters = {}) {
232
233
  const rows = db.prepare(sql).all(...params);
233
234
  return rows.map((r) => ({
234
235
  ...r,
235
- details: r.details ? JSON.parse(r.details) : null,
236
+ // One corrupt details cell must not throw out of the whole query.
237
+ details: safeJsonParse(r.details, null),
236
238
  success: r.success === 1,
237
239
  }));
238
240
  }
@@ -13,8 +13,8 @@ import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
13
13
  import { appendTokenUsage } from "../harness/jsonl-session-store.js";
14
14
  import {
15
15
  isRetryableStreamError,
16
- STREAM_RETRY_MAX,
17
16
  STREAM_RETRY_BASE_MS,
17
+ resolveStreamRetryMax,
18
18
  } from "./stream-retry.js";
19
19
 
20
20
  // A streaming chat call must not hang forever if the API accepts the connection
@@ -513,6 +513,9 @@ export async function* chatStream(messages, options) {
513
513
  // any token still terminates the generator cleanly.
514
514
  const streamPromise = (async () => {
515
515
  try {
516
+ // Budget honors CC_MAX_RETRIES / CLAUDE_CODE_MAX_RETRIES (capped 15),
517
+ // same as the agent path.
518
+ const maxRetries = resolveStreamRetryMax();
516
519
  let attempt = 0;
517
520
  for (;;) {
518
521
  try {
@@ -524,7 +527,7 @@ export async function* chatStream(messages, options) {
524
527
  // zero-output retry safety (cc agent parity). Stall aborts (180s) and
525
528
  // HTTP/auth errors are not retryable and surface immediately.
526
529
  if (
527
- attempt >= STREAM_RETRY_MAX ||
530
+ attempt >= maxRetries ||
528
531
  tokensEmitted > 0 ||
529
532
  !isRetryableStreamError(err)
530
533
  ) {
@@ -22,6 +22,40 @@ import { firstBalancedJson } from "./json-schema-output.js";
22
22
 
23
23
  const DEFAULT_INTENT_TIMEOUT_MS = 15000;
24
24
 
25
+ /**
26
+ * Resolve the per-call intent-classification budget. `llmOptions.intentTimeoutMs`
27
+ * overrides the default; an explicit 0 disables the cap; NaN/negative/absent →
28
+ * default.
29
+ */
30
+ function resolveIntentTimeout(llmOptions) {
31
+ const raw = llmOptions?.intentTimeoutMs;
32
+ if (raw === 0) return 0; // explicit disable
33
+ const n = Number(raw);
34
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_INTENT_TIMEOUT_MS;
35
+ }
36
+
37
+ /**
38
+ * Bound an LLM call to the intent budget. chat-core has its OWN stall guard, but
39
+ * it only fires on SILENCE (no bytes for 180s) — a slow trickling stream never
40
+ * trips it, so without this the declared 15s intent budget was never enforced
41
+ * and classification could block for minutes. Racing the deadline makes it fail
42
+ * fast into the caller's graceful rule/verbatim fallback. `ms <= 0` disables.
43
+ * The losing background request is harmless (chat-core releases its own socket);
44
+ * the timer is unref'd so it never holds the process open.
45
+ */
46
+ function withIntentTimeout(promise, ms) {
47
+ if (!(Number(ms) > 0)) return promise;
48
+ let timer;
49
+ const timeout = new Promise((_resolve, reject) => {
50
+ timer = setTimeout(
51
+ () => reject(new Error(`intent LLM call timed out after ${ms}ms`)),
52
+ Number(ms),
53
+ );
54
+ if (timer && typeof timer.unref === "function") timer.unref();
55
+ });
56
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
57
+ }
58
+
25
59
  /**
26
60
  * Build the V5 understandIntent system + user prompt pair.
27
61
  *
@@ -123,17 +157,20 @@ export async function understandIntent({
123
157
  );
124
158
 
125
159
  try {
126
- const fullContent = await chatWithStreaming(
127
- [
128
- { role: "system", content: systemPrompt },
129
- { role: "user", content: userPrompt },
130
- ],
131
- {
132
- ...llmOptions,
133
- // Lower temperature → more deterministic JSON output.
134
- temperature: 0.3,
135
- maxTokens: 500,
136
- },
160
+ const fullContent = await withIntentTimeout(
161
+ chatWithStreaming(
162
+ [
163
+ { role: "system", content: systemPrompt },
164
+ { role: "user", content: userPrompt },
165
+ ],
166
+ {
167
+ ...llmOptions,
168
+ // Lower temperature → more deterministic JSON output.
169
+ temperature: 0.3,
170
+ maxTokens: 500,
171
+ },
172
+ ),
173
+ resolveIntentTimeout(llmOptions),
137
174
  );
138
175
 
139
176
  const jsonText = extractJson(fullContent);
@@ -204,6 +241,11 @@ export async function* understandIntentStream({
204
241
  );
205
242
  let buffer = "";
206
243
  try {
244
+ // Incremental token yielding makes a per-call deadline awkward to apply
245
+ // here; this streaming variant is instead bounded by chat-core's own
246
+ // silence-based stall guard (CC_CHAT_STALL_MS, default 180s). The awaited
247
+ // understandIntent / classifyFollowupIntent paths enforce the tighter
248
+ // intent budget (resolveIntentTimeout) directly.
207
249
  for await (const event of chatStream(
208
250
  [
209
251
  { role: "system", content: systemPrompt },
@@ -452,16 +494,19 @@ ${
452
494
 
453
495
  async function llmBasedClassify(userInput, context, llmOptions) {
454
496
  const { systemPrompt, userPrompt } = buildFollowupPrompts(userInput, context);
455
- const fullContent = await chatWithStreaming(
456
- [
457
- { role: "system", content: systemPrompt },
458
- { role: "user", content: userPrompt },
459
- ],
460
- {
461
- ...llmOptions,
462
- temperature: 0.1,
463
- maxTokens: 300,
464
- },
497
+ const fullContent = await withIntentTimeout(
498
+ chatWithStreaming(
499
+ [
500
+ { role: "system", content: systemPrompt },
501
+ { role: "user", content: userPrompt },
502
+ ],
503
+ {
504
+ ...llmOptions,
505
+ temperature: 0.1,
506
+ maxTokens: 300,
507
+ },
508
+ ),
509
+ resolveIntentTimeout(llmOptions),
465
510
  );
466
511
  const jsonText = extractJson(fullContent);
467
512
  if (!jsonText) throw new Error("LLM response missing JSON");
@@ -540,5 +585,7 @@ export const _internal = {
540
585
  buildFollowupPrompts,
541
586
  extractJson,
542
587
  ruleBasedClassify,
588
+ resolveIntentTimeout,
589
+ withIntentTimeout,
543
590
  DEFAULT_INTENT_TIMEOUT_MS,
544
591
  };
@@ -11,6 +11,16 @@ import { getConfigPath } from "./paths.js";
11
11
  import { DEFAULT_CONFIG } from "../constants.js";
12
12
  import { withFileLock } from "./with-file-lock.js";
13
13
 
14
+ // Warn at most once per config path per process. Injectable seam: the default
15
+ // stays silent under vitest so test output isn't polluted; tests override
16
+ // `_deps.warn` to assert it fired.
17
+ const _warnedConfigPaths = new Set();
18
+ export const _deps = {
19
+ warn: (msg) => {
20
+ if (!process.env.VITEST) process.stderr.write(msg);
21
+ },
22
+ };
23
+
14
24
  export function loadConfig() {
15
25
  const configPath = getConfigPath();
16
26
  if (!existsSync(configPath)) {
@@ -20,7 +30,21 @@ export function loadConfig() {
20
30
  const raw = readFileSync(configPath, "utf-8");
21
31
  const parsed = JSON.parse(raw);
22
32
  return deepMerge(structuredClone(DEFAULT_CONFIG), parsed);
23
- } catch {
33
+ } catch (err) {
34
+ // The file EXISTS but couldn't be read/parsed (a typo, trailing comma, or
35
+ // truncated write). Silently returning defaults drops the user's entire
36
+ // config — provider, model, baseUrl, API key — and falls back to the
37
+ // default provider, producing a baffling "configured X but it runs Y / says
38
+ // no API key". Warn once so the failure is visible; still fall back so cc
39
+ // keeps working.
40
+ if (!_warnedConfigPaths.has(configPath)) {
41
+ _warnedConfigPaths.add(configPath);
42
+ _deps.warn(
43
+ `⚠️ Could not read config at ${configPath} (${err.message}). ` +
44
+ `Using defaults — your saved settings (provider / model / API key) are being IGNORED. ` +
45
+ `Fix the JSON to restore them.\n`,
46
+ );
47
+ }
24
48
  return { ...structuredClone(DEFAULT_CONFIG) };
25
49
  }
26
50
  }
@@ -149,7 +149,10 @@ export function credentialFileReasonResolved(targetPath, opts = {}) {
149
149
  // ── secret ENV-VAR + command detection ──────────────────────────────────────
150
150
 
151
151
  // Content-reader commands: when one of these is aimed at a credential file, the
152
- // file's bytes land in the agent's tool output.
152
+ // file's bytes land in the agent's tool output. Includes text processors
153
+ // (awk/sed), encoders (base64), and field/byte slicers — all of which dump a
154
+ // file's content just like `cat`, so an agent told to "read the .env" can't
155
+ // route around the guard with `base64 .env` / `awk '{print}' .env`.
153
156
  const READ_COMMANDS = new Set([
154
157
  "cat",
155
158
  "tac",
@@ -165,8 +168,31 @@ const READ_COMMANDS = new Set([
165
168
  "xxd",
166
169
  "od",
167
170
  "strings",
171
+ "awk",
172
+ "gawk",
173
+ "mawk",
174
+ "sed",
175
+ "base64",
176
+ "base32",
177
+ "cut",
178
+ "rev",
179
+ "sort",
180
+ "uniq",
181
+ "tr",
182
+ "fold",
183
+ "expand",
184
+ "unexpand",
185
+ "column",
186
+ "paste",
187
+ "pr",
168
188
  ]);
169
189
 
190
+ // grep-family is a content-reader too, but its FIRST non-flag arg is the search
191
+ // PATTERN (which may itself look credential-ish, e.g. `grep "id_rsa" notes.txt`)
192
+ // — so it is handled separately, checking only the FILE args after the pattern,
193
+ // to keep the guard precise (no false positive on the pattern).
194
+ const GREP_COMMANDS = new Set(["grep", "egrep", "fgrep", "rg", "ag"]);
195
+
170
196
  // Commands whose job is to print a value into output — the exfil surface for a
171
197
  // secret env var (a tool that merely CONSUMES `$API_KEY` does not echo it).
172
198
  const PRINT_COMMANDS = new Set([
@@ -281,6 +307,22 @@ export function commandReadsCredentials(command) {
281
307
  }
282
308
  }
283
309
 
310
+ // (b2) grep-family: skip the search pattern (first non-flag arg) and check
311
+ // only the file args, so `grep KEY .env` is caught but `grep "id_rsa"
312
+ // notes.txt` (searching FOR that text) is not a false positive.
313
+ if (GREP_COMMANDS.has(first)) {
314
+ for (const tok of nonFlagArgs.slice(1)) {
315
+ const r = credentialFileReason(tok);
316
+ if (r)
317
+ return {
318
+ reason: `reads ${r}`,
319
+ kind: "file",
320
+ target: tok,
321
+ segment: seg,
322
+ };
323
+ }
324
+ }
325
+
284
326
  // (c) printing / reading a secret-looking env var (via $VAR / %VAR% / env:).
285
327
  if (PRINT_COMMANDS.has(first) || READ_COMMANDS.has(first)) {
286
328
  const vars = referencedSecretVars(seg);
@@ -105,6 +105,11 @@ export function gitExec(args, cwd) {
105
105
  return execSync(`git ${args}`, {
106
106
  cwd,
107
107
  encoding: "utf-8",
108
+ // git diff/log/worktree-list output on a large repo easily exceeds
109
+ // execSync's 1 MB default → ENOBUFS. Match gitExecArgs' 64 MB ceiling so
110
+ // a big diff/log doesn't spuriously fail (e.g. worktree-diff's --numstat
111
+ // pass, which isn't wrapped in a degrade-gracefully try/catch).
112
+ maxBuffer: 64 * 1024 * 1024,
108
113
  stdio: ["pipe", "pipe", "pipe"],
109
114
  }).trim();
110
115
  } catch (err) {
@@ -159,7 +159,15 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
159
159
  },
160
160
  options.timeoutMs || 5 * 60 * 1000,
161
161
  );
162
- this._pending.set(requestId, { resolve, reject, timeoutId });
162
+ // `kind` tags what KIND of response settles this request ("question" vs
163
+ // "host-tool"). Checked in _resolvePending so a peer's session-answer can
164
+ // never resolve a host-tool call with a plain string (or vice versa).
165
+ this._pending.set(requestId, {
166
+ resolve,
167
+ reject,
168
+ timeoutId,
169
+ kind: options.kind || null,
170
+ });
163
171
 
164
172
  this._sendWs({
165
173
  ...message,
@@ -170,12 +178,15 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
170
178
  }
171
179
 
172
180
  _ask(questionType, question, extra = {}) {
173
- return this._request({
174
- type: "question",
175
- questionType,
176
- question,
177
- ...extra,
178
- });
181
+ return this._request(
182
+ {
183
+ type: "question",
184
+ questionType,
185
+ question,
186
+ ...extra,
187
+ },
188
+ { kind: "question" },
189
+ );
179
190
  }
180
191
 
181
192
  async askInput(question, options = {}) {
@@ -200,7 +211,7 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
200
211
  * Resolves the corresponding pending promise.
201
212
  */
202
213
  resolveAnswer(requestId, answer) {
203
- this._resolvePending(requestId, answer);
214
+ this._resolvePending(requestId, answer, "question");
204
215
  }
205
216
 
206
217
  async requestHostTool(toolName, args = {}, extra = {}) {
@@ -211,12 +222,12 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
211
222
  args,
212
223
  ...extra,
213
224
  },
214
- { timeoutMs: extra.timeoutMs || 60 * 1000 },
225
+ { timeoutMs: extra.timeoutMs || 60 * 1000, kind: "host-tool" },
215
226
  );
216
227
  }
217
228
 
218
229
  resolveHostTool(requestId, payload) {
219
- this._resolvePending(requestId, payload);
230
+ this._resolvePending(requestId, payload, "host-tool");
220
231
  }
221
232
 
222
233
  rejectAllPending(reason = createAbortError("Interaction interrupted")) {
@@ -310,11 +321,21 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
310
321
  }
311
322
  }
312
323
 
313
- _resolvePending(requestId, payload) {
324
+ _resolvePending(requestId, payload, expectedKind = null) {
314
325
  const pending = this._pending.get(requestId);
315
326
  if (!pending) {
316
327
  return;
317
328
  }
329
+ // Kind guard: a question-answer must not settle a host-tool request (or
330
+ // vice versa). On a mismatch, IGNORE the message — leave the request pending
331
+ // so its correct resolver (or its timeout) can still settle it. Prevents a
332
+ // peer from resolving a host-tool call with a plain question string, or a
333
+ // question with a structured tool payload (type confusion of the awaited
334
+ // value). Backward-compatible: callers passing no expectedKind, and
335
+ // pre-kind pending entries, behave exactly as before.
336
+ if (expectedKind && pending.kind && pending.kind !== expectedKind) {
337
+ return;
338
+ }
318
339
 
319
340
  this._pending.delete(requestId);
320
341
  if (pending.timeoutId) {
@@ -32,4 +32,5 @@ export {
32
32
  sessionPath,
33
33
  isUnsafeSessionId,
34
34
  appendTokenUsage,
35
+ toIsoSafe,
35
36
  } from "../harness/jsonl-session-store.js";
@@ -17,6 +17,43 @@
17
17
  * lesson as the --settings "opus"→404 vision trap.
18
18
  * - No configured provider → unchanged (runner defaults apply: ollama).
19
19
  */
20
+ /**
21
+ * Reconcile a MISLABELED llm config/options object. The `baseUrl` is the
22
+ * authoritative signal of which provider you actually reach, so when `provider`
23
+ * disagrees with the provider its `baseUrl` belongs to, the config is simply
24
+ * mislabeled — the recurring "provider:anthropic + volces baseUrl + model:haiku"
25
+ * corruption that makes runnable-provider relabel (and emit "provider 配置与
26
+ * baseUrl 不一致") on EVERY run because the on-disk config never gets fixed.
27
+ *
28
+ * Correct `provider` to match the endpoint, and if the model is foreign to the
29
+ * corrected provider, replace it with that provider's default. Pure: the input
30
+ * is never mutated — returns `{ llm, changed }` where `llm` is the original
31
+ * object (changed:false) or a corrected shallow copy (changed:true). Works for
32
+ * both a `config.llm` block and a resolved request-`options` (same shape).
33
+ *
34
+ * `inferProviderFromBaseUrl` returns null for unknown/custom hosts (proxies),
35
+ * so an intentionally-custom endpoint is left untouched.
36
+ */
37
+ export async function reconcileConfigLlmProvider(cfgLlm = {}) {
38
+ if (!cfgLlm || !cfgLlm.provider || !cfgLlm.baseUrl) {
39
+ return { llm: cfgLlm, changed: false };
40
+ }
41
+ const { inferProviderFromBaseUrl, modelForeignToProvider } =
42
+ await import("./runnable-provider.js");
43
+ const inferred = inferProviderFromBaseUrl(cfgLlm.baseUrl);
44
+ if (!inferred || inferred === cfgLlm.provider) {
45
+ return { llm: cfgLlm, changed: false };
46
+ }
47
+ const corrected = { ...cfgLlm, provider: inferred };
48
+ if (modelForeignToProvider(inferred, cfgLlm.model)) {
49
+ const { selectModelForTask, TaskType } =
50
+ await import("./task-model-selector.js");
51
+ const def = selectModelForTask(inferred, TaskType.CHAT);
52
+ if (def) corrected.model = def;
53
+ }
54
+ return { llm: corrected, changed: true };
55
+ }
56
+
20
57
  export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
21
58
  // The IDE chat panel pins --provider/--model (read from config) but DROPS
22
59
  // --base-url/--api-key. With an explicit --provider the block below bails, so