mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -1,4 +1,4 @@
1
- import Anthropic from '@anthropic-ai/sdk';
1
+ import { createRequire } from 'node:module';
2
2
  import { loadConfig } from '../config.mjs';
3
3
  import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
4
4
  import { classifyError, midstreamBackoffFor, sleepWithAbort, withRetry } from './retry-classifier.mjs';
@@ -15,10 +15,27 @@ import {
15
15
  _classifyMidstreamError,
16
16
  } from './anthropic-oauth.mjs';
17
17
  import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
18
+ import {
19
+ applyAnthropicEffortToBody,
20
+ effortValuesForModel,
21
+ shouldIncludeEffortBeta,
22
+ } from './anthropic-effort.mjs';
18
23
  import { normalizeContentForAnthropic } from './media-normalization.mjs';
19
24
  import { enrichModels } from './model-catalog.mjs';
25
+ import { makeModelCache } from './model-cache.mjs';
26
+ import { resolveAnthropicMaxTokens } from './anthropic-max-tokens.mjs';
20
27
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
21
28
 
29
+ const require = createRequire(import.meta.url);
30
+ let _Anthropic = null;
31
+ function loadAnthropic() {
32
+ if (!_Anthropic) {
33
+ const mod = require('@anthropic-ai/sdk');
34
+ _Anthropic = mod.default || mod.Anthropic || mod;
35
+ }
36
+ return _Anthropic;
37
+ }
38
+
22
39
  // Abort-aware mid-stream backoff sleep → shared sleepWithAbort
23
40
  // (retry-classifier.mjs). abortMessage preserves the prior fallback text.
24
41
  function _midstreamSleepWithAbort(ms, signal) {
@@ -117,6 +134,10 @@ function _defaultContextForModel(id, family) {
117
134
  return 200000;
118
135
  }
119
136
 
137
+ function _capabilitySupported(capability) {
138
+ return capability === true || capability?.supported === true;
139
+ }
140
+
120
141
  function _normalizeAnthropicModel(raw, provider = 'anthropic') {
121
142
  const id = raw?.id || raw?.name || raw?.model;
122
143
  if (!id) return null;
@@ -124,35 +145,64 @@ function _normalizeAnthropicModel(raw, provider = 'anthropic') {
124
145
  const family = familyMatch ? familyMatch[1].toLowerCase() : 'other';
125
146
  const dated = /-\d{8}$/.test(String(id));
126
147
  const versioned = !dated && /^claude-[a-z]+-\d+(?:-\d+)?$/i.test(String(id));
148
+ const effortValues = effortValuesForModel(raw?.capabilities, id);
127
149
  return {
128
150
  id,
129
151
  display: raw?.display_name || raw?.displayName || raw?.display || _prettyName(id, family),
130
152
  family,
131
153
  provider,
132
- contextWindow: raw?.context_window || raw?.max_context_window || raw?.input_token_limit || raw?.inputTokenLimit || _defaultContextForModel(id, family),
133
- outputTokens: raw?.max_output_tokens || raw?.output_token_limit || raw?.outputTokenLimit || null,
154
+ contextWindow: raw?.context_window || raw?.max_context_window || raw?.max_input_tokens || raw?.input_token_limit || raw?.inputTokenLimit || _defaultContextForModel(id, family),
155
+ outputTokens: raw?.max_tokens || raw?.max_output_tokens || raw?.output_token_limit || raw?.outputTokenLimit || null,
134
156
  tier: dated ? 'dated' : versioned ? 'version' : 'family',
135
157
  latest: false,
158
+ supportsReasoning: effortValues.length > 0 || _capabilitySupported(raw?.capabilities?.thinking),
159
+ reasoningOptions: effortValues.length ? [{ type: 'effort', values: effortValues }] : [],
136
160
  };
137
161
  }
138
162
  // Family-based heuristic so new model ids (including custom user-configured
139
163
  // ones) resolve a sensible max_tokens without requiring a code change.
164
+ // The API-key provider has no catalog cache of its own — it reads the same
165
+ // anthropic-oauth-models.json disk cache (read-only) that the OAuth provider
166
+ // maintains. Both providers hit the same Anthropic /v1/models catalog, so a
167
+ // per-model outputTokens entry is valid regardless of which auth path wrote
168
+ // it. If this provider is ever run standalone without the OAuth provider
169
+ // ever having populated the cache, loadSync() simply returns null and we
170
+ // fall through to the shared static heuristic in anthropic-max-tokens.mjs.
171
+ const ANTHROPIC_OAUTH_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
172
+ const _sharedOAuthModelCache = makeModelCache({
173
+ fileName: 'anthropic-oauth-models.json',
174
+ ttlMs: ANTHROPIC_OAUTH_MODEL_CACHE_TTL_MS,
175
+ version: 1,
176
+ });
177
+
178
+ // In-memory mirror populated by this provider's own listModels() fetch.
179
+ // API-key-only installs never have the OAuth provider write the shared disk
180
+ // cache, so without this mirror catalog outputTokens would stay invisible to
181
+ // resolveMaxTokens until an OAuth session runs. listModels() results flow in
182
+ // here (memory only — the disk cache stays OAuth-owned/read-only for us).
183
+ let _apiKeyCatalogMirror = null;
184
+
185
+ function _catalogOutputTokensFromSharedCache(model) {
186
+ if (!model) return null;
187
+ try {
188
+ const models = Array.isArray(_apiKeyCatalogMirror)
189
+ ? _apiKeyCatalogMirror
190
+ : _sharedOAuthModelCache.loadSync();
191
+ if (!Array.isArray(models)) return null;
192
+ const entry = models.find(m => m?.id === model);
193
+ const out = Number(entry?.outputTokens);
194
+ return Number.isFinite(out) && out > 0 ? out : null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
140
200
  function resolveMaxTokens(model) {
141
- const id = String(model || '').toLowerCase();
142
- if (id.includes('opus')) return 32768;
143
- if (id.includes('sonnet')) return 16384;
144
- if (id.includes('haiku')) return 8192;
145
- return 8192;
201
+ return resolveAnthropicMaxTokens(model, { catalogLookup: _catalogOutputTokensFromSharedCache });
146
202
  }
147
203
 
148
- // Effort thinking budget tokens (Anthropic extended thinking)
149
- const EFFORT_BUDGET = {
150
- low: 1024,
151
- medium: 4096,
152
- high: 16384,
153
- xhigh: 32768,
154
- max: 32768,
155
- };
204
+ // Test-only escape hatch for scripts/anthropic-maxtokens-test.mjs.
205
+ export const _test = { resolveMaxTokens };
156
206
 
157
207
  const MIN_THINKING_BUDGET = 1024;
158
208
  const THINKING_OUTPUT_RESERVE = 1024;
@@ -389,7 +439,7 @@ export class AnthropicProvider {
389
439
  this.config = config;
390
440
  this.name = config.name || 'anthropic';
391
441
  const betaHeaders = config.disableBetaHeaders ? null : buildAnthropicBetaHeaders({ toolSearch: true });
392
- this.client = new Anthropic({
442
+ this.client = new (loadAnthropic())({
393
443
  apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
394
444
  ...(config.baseURL ? { baseURL: config.baseURL } : {}),
395
445
  defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(config.extraHeaders || {}) },
@@ -403,7 +453,7 @@ export class AnthropicProvider {
403
453
  if (newKey) {
404
454
  this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey };
405
455
  const betaHeaders = this.config.disableBetaHeaders ? null : buildAnthropicBetaHeaders({ toolSearch: true });
406
- this.client = new Anthropic({
456
+ this.client = new (loadAnthropic())({
407
457
  apiKey: newKey,
408
458
  ...(this.config.baseURL ? { baseURL: this.config.baseURL } : {}),
409
459
  defaultHeaders: { ...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}), ...(this.config.extraHeaders || {}) },
@@ -451,8 +501,7 @@ export class AnthropicProvider {
451
501
  const usedSlots = toolsBpUsed + systemBpUsed;
452
502
  // Env override for BP strategy. ANTHROPIC_MSG_SLOTS=0 disables
453
503
  // message caching entirely. Any value >=1 first marks the previous
454
- // user text turn; a second free slot marks the tail. Mirrors
455
- // anthropic-oauth.mjs for twin parity.
504
+ // user text turn; a second free slot marks the tail.
456
505
  const msgSlotsCap = Number.parseInt(process.env.ANTHROPIC_MSG_SLOTS, 10);
457
506
  const defaultMsgSlots = Math.max(0, 4 - usedSlots);
458
507
  const msgSlots = ttls.messages
@@ -481,16 +530,21 @@ export class AnthropicProvider {
481
530
  // Anthropic prefix semantics (order: tools → system → messages).
482
531
  params.tools = [...nativeTools, ...toAnthropicTools([...(tools || []), ...deferredAnthropicTools(tools || [], opts)])];
483
532
  }
484
- // Effort extended thinking budget. Gateway inherit mode may pass the
485
- // exact OAuth client budget from the incoming Anthropic request.
486
- const thinkingBudgetTokens = Number(opts.thinkingBudgetTokens);
487
- const requestedThinkingBudget = Number.isFinite(thinkingBudgetTokens) && thinkingBudgetTokens > 0
488
- ? thinkingBudgetTokens
489
- : (opts.effort && EFFORT_BUDGET[opts.effort] ? EFFORT_BUDGET[opts.effort] : null);
490
- const budgetTokens = clampThinkingBudgetTokens(requestedThinkingBudget, maxTokens);
491
- if (budgetTokens) {
492
- params.thinking = { type: 'enabled', budget_tokens: budgetTokens };
493
- }
533
+ // Known tool names for the shared parseSSEStream leaked-tool-call guard
534
+ // (same guard fixes both Anthropic providers). Recovered leaked calls
535
+ // are only synthesized when they name a tool actually offered here.
536
+ const knownToolNames = new Set(
537
+ (Array.isArray(params.tools) ? params.tools : [])
538
+ .map((t) => (t && typeof t.name === 'string' ? t.name : null))
539
+ .filter(Boolean),
540
+ );
541
+ applyAnthropicEffortToBody(params, {
542
+ model: useModel,
543
+ opts,
544
+ maxTokens,
545
+ clampThinkingBudgetTokens,
546
+ logTag: this.name,
547
+ });
494
548
  // Fast mode → speed: "fast" on models Anthropic marks as speed-capable.
495
549
  if (opts.fast === true && supportsAnthropicFastMode(useModel)) {
496
550
  params.speed = 'fast';
@@ -524,12 +578,20 @@ export class AnthropicProvider {
524
578
  try { totalSignal.removeEventListener('abort', handler); } catch {}
525
579
  };
526
580
 
527
- const betaHeaders = {
528
- 'anthropic-beta': buildAnthropicBetaHeaders({
529
- fastMode: this.fastModeBetaHeaderLatched,
530
- toolSearch: true,
531
- }),
532
- };
581
+ // Per-call headers override the client defaultHeaders, so the
582
+ // constructor-level disableBetaHeaders opt-out must be honoured here
583
+ // too — otherwise opencode-go's anthropic-compatible routing
584
+ // (disableBetaHeaders:true) would still send beta strings that a
585
+ // third-party endpoint may reject.
586
+ const betaHeaders = this.config?.disableBetaHeaders
587
+ ? null
588
+ : {
589
+ 'anthropic-beta': buildAnthropicBetaHeaders({
590
+ fastMode: this.fastModeBetaHeaderLatched,
591
+ toolSearch: true,
592
+ effort: shouldIncludeEffortBeta(useModel, opts),
593
+ }),
594
+ };
533
595
 
534
596
  const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
535
597
  let firstAttemptError = null;
@@ -623,7 +685,7 @@ export class AnthropicProvider {
623
685
  async ({ signal: attemptSignal }) => {
624
686
  const res = await this.client.messages.create(params, {
625
687
  signal: attemptSignal,
626
- headers: betaHeaders,
688
+ ...(betaHeaders ? { headers: betaHeaders } : {}),
627
689
  }).asResponse();
628
690
  if (!res.ok) {
629
691
  const text = await res.text().catch(() => '');
@@ -676,11 +738,12 @@ export class AnthropicProvider {
676
738
  const parseResult = await parseSSEStream(
677
739
  response,
678
740
  streamController.signal,
679
- () => streamController.abort(),
741
+ (reason) => streamController.abort(reason),
680
742
  onStreamDelta,
681
743
  onToolCall,
682
744
  midState,
683
745
  onTextDelta,
746
+ knownToolNames,
684
747
  );
685
748
 
686
749
  if (firstBytePoll) {
@@ -799,6 +862,9 @@ export class AnthropicProvider {
799
862
  .map((m) => _normalizeAnthropicModel(m, this.name))
800
863
  .filter(Boolean);
801
864
  const enriched = await enrichModels(normalized);
865
+ // Feed the resolver-visible mirror so API-key-only installs get
866
+ // catalog outputTokens without depending on the OAuth disk cache.
867
+ if (enriched.length) _apiKeyCatalogMirror = enriched.slice();
802
868
  return enriched.length ? enriched : MODELS;
803
869
  }
804
870
  catch (err) {
@@ -1,8 +1,9 @@
1
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
- import * as fsp from 'node:fs/promises';
1
+ import { existsSync, readFileSync } from 'node:fs';
3
2
  import { join } from 'node:path';
3
+ import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
4
4
  import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
5
5
  import { getAgentApiKey, getOpenAIUsageSessionKey } from '../../../shared/config.mjs';
6
+ import { num, round } from './lib/usage-primitives.mjs';
6
7
 
7
8
  const CACHE_FILE = 'api-usage-cache.json';
8
9
  const LIVE_TTL_MS = 5 * 60_000;
@@ -10,19 +11,6 @@ const STALE_TTL_MS = 60 * 60_000;
10
11
  const DISK_JSON_MEMORY_TTL_MS = 1000;
11
12
  let diskJsonCache = { at: 0, file: '', value: null };
12
13
 
13
- function num(value, fallback = null) {
14
- if (value === null || value === undefined || value === '') return fallback;
15
- const n = Number(value);
16
- return Number.isFinite(n) ? n : fallback;
17
- }
18
-
19
- function round(value, digits = 4) {
20
- const n = Number(value);
21
- if (!Number.isFinite(n)) return null;
22
- const scale = 10 ** digits;
23
- return Math.round(n * scale) / scale;
24
- }
25
-
26
14
  function cachePath() {
27
15
  return join(resolvePluginData(), CACHE_FILE);
28
16
  }
@@ -41,14 +29,6 @@ function readJson(file) {
41
29
  }
42
30
  }
43
31
 
44
- function writeJson(file, value) {
45
- diskJsonCache = { at: Date.now(), file, value };
46
- try {
47
- mkdirSync(resolvePluginData(), { recursive: true });
48
- void fsp.writeFile(file, JSON.stringify(value, null, 2), 'utf8').catch(() => {});
49
- } catch {}
50
- }
51
-
52
32
  function cacheKey(provider) {
53
33
  return String(provider || '').trim().toLowerCase();
54
34
  }
@@ -68,16 +48,31 @@ export function readCachedApiUsageSnapshot(provider, { allowStale = true } = {})
68
48
 
69
49
  function writeCachedApiUsageSnapshot(provider, snapshot) {
70
50
  const file = cachePath();
71
- const raw = readJson(file) || {};
72
- const snapshots = raw.snapshots && typeof raw.snapshots === 'object' ? raw.snapshots : {};
73
- writeJson(file, {
74
- version: 1,
75
- updatedAt: Date.now(),
76
- snapshots: {
77
- ...snapshots,
78
- [cacheKey(provider)]: snapshot,
79
- },
80
- });
51
+ // Synchronous atomic+lock write (updateJsonAtomicSync) rather than the
52
+ // prior fire-and-forget fsp.writeFile: the read-modify-write of
53
+ // `snapshots` must happen inside the same file lock as the write, or two
54
+ // concurrent orchestrator processes updating different providers can
55
+ // clobber each other's snapshot merge (last writer wins, losing the
56
+ // other's entry). Usage snapshot writes are infrequent (once per
57
+ // provider per TTL window, not per-request), so trading the async/
58
+ // non-blocking write for lock+fsync-dir safety has no meaningful
59
+ // latency impact on the request path.
60
+ let next = null;
61
+ try {
62
+ next = updateJsonAtomicSync(file, (curRaw) => {
63
+ const cur = curRaw && typeof curRaw === 'object' ? curRaw : {};
64
+ const snapshots = cur.snapshots && typeof cur.snapshots === 'object' ? cur.snapshots : {};
65
+ return {
66
+ version: 1,
67
+ updatedAt: Date.now(),
68
+ snapshots: {
69
+ ...snapshots,
70
+ [cacheKey(provider)]: snapshot,
71
+ },
72
+ };
73
+ }, { lock: true, fsyncDir: true, timeoutMs: 1000 }); // best-effort cache write: short lock timeout, don't block on contention
74
+ } catch {}
75
+ if (next) diskJsonCache = { at: Date.now(), file, value: next };
81
76
  }
82
77
 
83
78
  function authHeaders(key, extra = {}) {
@@ -16,6 +16,7 @@ import {
16
16
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
17
17
  import { traceHash, stableTraceStringify, summarizeTraceTools, traceTextShape } from './trace-utils.mjs';
18
18
  import { normalizeContentForGeminiParts, splitToolContentForGemini } from './media-normalization.mjs';
19
+ import { scanLeakedToolCalls } from './anthropic-leaked-toolcall.mjs';
19
20
 
20
21
  const MODELS = [
21
22
  { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'gemini', contextWindow: 1048576 },
@@ -31,12 +32,19 @@ const DEFAULT_MODEL = MODELS[0].id;
31
32
  // Gemini's /models has no `created` timestamp, so latest-resolution is
32
33
  // VERSION-based (parse gemini-X.Y) rather than release-date based.
33
34
  const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
35
+ // Bump when the on-disk cache shape changes so stale-shape entries are
36
+ // discarded instead of misread.
37
+ const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 1;
34
38
 
35
39
  // De-dupes concurrent force-refreshes so they share one HTTP round-trip,
36
40
  // mirroring anthropic-oauth's _modelRefreshInFlight.
37
41
  let _modelRefreshInFlight = null;
38
42
 
39
- const _modelCache = makeModelCache({ fileName: 'gemini-models.json', ttlMs: MODEL_CACHE_TTL_MS });
43
+ const _modelCache = makeModelCache({
44
+ fileName: 'gemini-models.json',
45
+ ttlMs: MODEL_CACHE_TTL_MS,
46
+ version: GEMINI_MODEL_CACHE_SCHEMA_VERSION,
47
+ });
40
48
 
41
49
  // Mirror of anthropic-oauth.mjs _compareVersion: compare two gemini ids by the
42
50
  // X.Y version embedded in the id (gemini-3.5-flash -> [3, 5]). Falls back to a
@@ -182,6 +190,10 @@ function _geminiCachePrefixHash({ model, systemInstruction, geminiTools, content
182
190
 
183
191
  const GEMINI_GLOBAL_CACHE_MIN_LIVE_MS = 6 * 60 * 1000;
184
192
  const GEMINI_GLOBAL_CACHE_MAX_ENTRIES = 128;
193
+ // Grace window before deleting a superseded cachedContents name (see the
194
+ // cross-session race note at the L1341-1372 call site). Long enough that a
195
+ // concurrent session still mid-flight on the old name has time to finish.
196
+ const GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS = 2 * 60 * 1000;
185
197
  const geminiGlobalCaches = new Map();
186
198
  const geminiGlobalCacheCreates = new Map();
187
199
 
@@ -294,7 +306,7 @@ function writeGeminiCacheTrace({ opts, model, systemInstruction, tools, contents
294
306
  provider: 'gemini',
295
307
  model,
296
308
  owner: session.owner || null,
297
- role: session.role || null,
309
+ agent: session.agent || null,
298
310
  permission: session.permission || null,
299
311
  toolPermission: session.toolPermission || null,
300
312
  profileId: session.profileId || null,
@@ -412,7 +424,104 @@ function geminiChunkText(chunk) {
412
424
  return text;
413
425
  }
414
426
 
415
- async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta, onTextDelta, label }) {
427
+ function relayGeminiStreamText(t, { onTextDelta, textLeakGuard }) {
428
+ if (!t) return;
429
+ if (textLeakGuard) textLeakGuard.feedText(t);
430
+ else if (onTextDelta) { try { onTextDelta(t); } catch {} }
431
+ }
432
+
433
+ /**
434
+ * Rolling scanner for tool calls leaked as plain XML/antml tags inside Gemini
435
+ * `part.text` streams. Mirrors the Anthropic OAuth guard: suppress tags from
436
+ * visible text, synthesize known-tool calls, dispatch via onToolCall.
437
+ */
438
+ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolCall, onStreamDelta }) {
439
+ const _knownTools = knownToolNames instanceof Set
440
+ ? knownToolNames
441
+ : new Set(Array.isArray(knownToolNames) ? knownToolNames : []);
442
+ const _enabled = _knownTools.size > 0;
443
+ const _isKnownTool = (name) => _knownTools.has(name);
444
+ let leakBuffer = '';
445
+ const leakedCalls = [];
446
+ const dispatchedFingerprints = new Set();
447
+
448
+ const toolCallFingerprint = (name, args) => {
449
+ let a = args;
450
+ if (a === null || typeof a !== 'object' || Array.isArray(a)) a = {};
451
+ return traceHash(stableTraceStringify({ name: name || '', args: a }));
452
+ };
453
+
454
+ const dispatchLeakedCall = (recovered) => {
455
+ let args = recovered?.arguments;
456
+ if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
457
+ const fp = toolCallFingerprint(recovered.name, args);
458
+ if (dispatchedFingerprints.has(fp)) return;
459
+ dispatchedFingerprints.add(fp);
460
+ const idHash = traceHash(stableTraceStringify({
461
+ name: recovered.name,
462
+ args,
463
+ leak: true,
464
+ })).slice(0, 16);
465
+ const call = {
466
+ id: `gemini_leaked_${idHash}`,
467
+ name: recovered.name,
468
+ arguments: args,
469
+ };
470
+ leakedCalls.push(call);
471
+ try { onToolCall?.(call); } catch {}
472
+ try { onStreamDelta?.(); } catch {}
473
+ };
474
+
475
+ const pumpLeakBuffer = (final) => {
476
+ if (!_enabled) return;
477
+ if (!leakBuffer && !final) return;
478
+ const { emit, calls, rest } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final });
479
+ leakBuffer = rest;
480
+ if (emit && onTextDelta) {
481
+ try { onTextDelta(emit); } catch {}
482
+ }
483
+ for (const c of calls) dispatchLeakedCall(c);
484
+ };
485
+
486
+ return {
487
+ get enabled() { return _enabled; },
488
+ feedText(text) {
489
+ if (!text) return;
490
+ if (!_enabled) {
491
+ try { onTextDelta?.(text); } catch {}
492
+ return;
493
+ }
494
+ leakBuffer += text;
495
+ pumpLeakBuffer(false);
496
+ },
497
+ finalize() {
498
+ pumpLeakBuffer(true);
499
+ },
500
+ scrubAssistantText(raw) {
501
+ if (!raw) return '';
502
+ if (!_enabled) return raw;
503
+ const { emit, calls, rest } = scanLeakedToolCalls(raw, { isKnownTool: _isKnownTool, final: true });
504
+ for (const c of calls) dispatchLeakedCall(c);
505
+ return emit + rest;
506
+ },
507
+ filterNativeToolCalls(nativeCalls) {
508
+ if (!_enabled || !nativeCalls?.length) return nativeCalls;
509
+ const kept = [];
510
+ for (const call of nativeCalls) {
511
+ const fp = toolCallFingerprint(call?.name, call?.arguments);
512
+ if (dispatchedFingerprints.has(fp)) continue;
513
+ dispatchedFingerprints.add(fp);
514
+ kept.push(call);
515
+ }
516
+ return kept.length ? kept : undefined;
517
+ },
518
+ getLeakedToolCalls() {
519
+ return leakedCalls.length ? [...leakedCalls] : [];
520
+ },
521
+ };
522
+ }
523
+
524
+ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta, onTextDelta, textLeakGuard, label }) {
416
525
  if (!response?.body) throw new Error(`${label}: missing response body`);
417
526
  const reader = response.body.getReader();
418
527
  const decoder = new TextDecoder();
@@ -509,9 +618,9 @@ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta
509
618
  }
510
619
  allChunks.push(parsed);
511
620
  try { onStreamDelta?.(); } catch {}
512
- if (onTextDelta) {
621
+ if (onTextDelta || textLeakGuard) {
513
622
  const t = geminiChunkText(parsed);
514
- if (t) { try { onTextDelta(t); } catch {} }
623
+ relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
515
624
  }
516
625
  }
517
626
  }
@@ -528,9 +637,9 @@ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta
528
637
  }
529
638
  allChunks.push(parsed);
530
639
  try { onStreamDelta?.(); } catch {}
531
- if (onTextDelta) {
640
+ if (onTextDelta || textLeakGuard) {
532
641
  const t = geminiChunkText(parsed);
533
- if (t) { try { onTextDelta(t); } catch {} }
642
+ relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
534
643
  }
535
644
  } catch { /* skip malformed tail */ }
536
645
  }
@@ -541,6 +650,7 @@ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta
541
650
  if (idleTimer) clearTimeout(idleTimer);
542
651
  if (signal) signal.removeEventListener('abort', onAbort);
543
652
  try { reader.releaseLock(); } catch {}
653
+ try { textLeakGuard?.finalize(); } catch {}
544
654
  }
545
655
 
546
656
  const aggregated = aggregateGeminiStreamChunks(allChunks);
@@ -549,7 +659,7 @@ async function consumeGeminiRestStreamResponse(response, { signal, onStreamDelta
549
659
  return aggregated;
550
660
  }
551
661
 
552
- async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onTextDelta, label }) {
662
+ async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onTextDelta, textLeakGuard, label }) {
553
663
  let sawStreamChunk = false;
554
664
  let idleTimedOut = false;
555
665
  let idleTimer = null;
@@ -665,9 +775,9 @@ async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onT
665
775
  }
666
776
  resetIdleTimer();
667
777
  try { onStreamDelta?.(); } catch {}
668
- if (onTextDelta) {
778
+ if (onTextDelta || textLeakGuard) {
669
779
  const t = geminiChunkText(step.value);
670
- if (t) { try { onTextDelta(t); } catch {} }
780
+ relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
671
781
  }
672
782
  }
673
783
  if (idleTimedOut) {
@@ -686,6 +796,7 @@ async function consumeGeminiSdkStream(streamResult, { signal, onStreamDelta, onT
686
796
  if (signal && onSignalAbort) {
687
797
  try { signal.removeEventListener('abort', onSignalAbort); } catch {}
688
798
  }
799
+ try { textLeakGuard?.finalize(); } catch {}
689
800
  }
690
801
 
691
802
  let response;
@@ -1241,11 +1352,44 @@ export class GeminiProvider {
1241
1352
  // Best-effort cleanup of the previous cache so storage cost only
1242
1353
  // accrues on the live revision. Fire-and-forget; TTL expiry covers
1243
1354
  // any delete failures.
1355
+ //
1356
+ // Cross-session race: `_geminiGlobalCacheNameIsLive` only checks
1357
+ // whether `priorCacheName` still appears as *some* entry's live
1358
+ // cacheName in `geminiGlobalCaches`. If another session sharing the
1359
+ // same globalCacheKey already overwrote that map slot with a newer
1360
+ // cache (via `_setGeminiGlobalCache`), the check sees "not live" for
1361
+ // a name that a *different* in-flight session still holds in its own
1362
+ // `providerState.gemini.cacheName` (captured earlier via
1363
+ // `_attachGeminiCacheState` and possibly already in-flight inside a
1364
+ // `generateContent`/`streamGenerateContent` call at L1470-1473).
1365
+ // Deleting immediately can 404 that concurrent request server-side.
1366
+ //
1367
+ // Fix chosen: delay the DELETE by a grace period instead of adding
1368
+ // refcounting/last-used-session tracking. Rationale (minimal-change,
1369
+ // matches the module's existing "best-effort, TTL is the backstop"
1370
+ // posture at L1342-1343):
1371
+ // - Any session that captured `priorCacheName` did so before this
1372
+ // create finished, so its in-flight (or next) turn using that
1373
+ // name almost certainly completes within a couple of minutes;
1374
+ // a short grace window is enough for it to either finish or move
1375
+ // on to a fresh cache attach.
1376
+ // - The server-side cache TTL (1h) already reclaims any cache we
1377
+ // fail to delete, so skipping/delaying deletion is safe — it
1378
+ // only costs a little extra storage for at most the grace
1379
+ // window, never correctness.
1380
+ // - Refcounting/session tracking would need to plumb per-session
1381
+ // liveness into a shared map across concurrent providers, which
1382
+ // is a much larger change for a purely cosmetic cost saving.
1383
+ // Re-check liveness right before firing the DELETE too, in case the
1384
+ // name became live again (e.g. re-attached) during the wait.
1244
1385
  const priorCacheName = state?.cacheName || null;
1245
- if (priorCacheName && priorCacheName !== cacheName && !_geminiGlobalCacheNameIsLive(priorCacheName)) {
1246
- const delUrl = `https://generativelanguage.googleapis.com/v1beta/${priorCacheName}?key=${encodeURIComponent(apiKey)}`;
1247
- fetch(delUrl, { method: 'DELETE', signal: AbortSignal.timeout(10_000), dispatcher: getLlmDispatcher() })
1248
- .catch(() => { /* TTL expiry will reclaim it */ });
1386
+ if (priorCacheName && priorCacheName !== cacheName) {
1387
+ setTimeout(() => {
1388
+ if (_geminiGlobalCacheNameIsLive(priorCacheName)) return;
1389
+ const delUrl = `https://generativelanguage.googleapis.com/v1beta/${priorCacheName}?key=${encodeURIComponent(apiKey)}`;
1390
+ fetch(delUrl, { method: 'DELETE', signal: AbortSignal.timeout(10_000), dispatcher: getLlmDispatcher() })
1391
+ .catch(() => { /* TTL expiry will reclaim it */ });
1392
+ }, GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS).unref?.();
1249
1393
  }
1250
1394
  const createdAt = Date.now();
1251
1395
  const entry = {
@@ -1322,6 +1466,14 @@ export class GeminiProvider {
1322
1466
  const toolConfig = functionGeminiTools.length ? toGeminiToolConfig(opts.toolChoice) : undefined;
1323
1467
  try { opts.onStageChange?.('requesting'); } catch {}
1324
1468
 
1469
+ const buildTextLeakGuard = () => createGeminiTextLeakGuard({
1470
+ knownToolNames: tools?.map((t) => t.name).filter(Boolean) ?? [],
1471
+ onTextDelta,
1472
+ onToolCall,
1473
+ onStreamDelta,
1474
+ });
1475
+ let textLeakGuard = null;
1476
+
1325
1477
  // Explicit cachedContents (system + tools + prior-turn transcript).
1326
1478
  // Per Google docs, `tools` must be supplied on BOTH the cache create
1327
1479
  // call AND every subsequent generate_content call — the cache stores
@@ -1401,10 +1553,12 @@ export class GeminiProvider {
1401
1553
  err.status = res.status;
1402
1554
  throw err;
1403
1555
  }
1556
+ textLeakGuard = buildTextLeakGuard();
1404
1557
  return await consumeGeminiRestStreamResponse(res, {
1405
1558
  signal: attemptSignal,
1406
1559
  onStreamDelta,
1407
1560
  onTextDelta,
1561
+ textLeakGuard,
1408
1562
  label: 'Gemini REST streamGenerateContent',
1409
1563
  });
1410
1564
  },
@@ -1485,10 +1639,12 @@ export class GeminiProvider {
1485
1639
  // timer but KEEP the parent link attached so a later
1486
1640
  // abort during streaming still reaches the request.
1487
1641
  clearConnectTimer();
1642
+ textLeakGuard = buildTextLeakGuard();
1488
1643
  return await consumeGeminiSdkStream(streamResult, {
1489
1644
  signal: attemptSignal,
1490
1645
  onStreamDelta,
1491
1646
  onTextDelta,
1647
+ textLeakGuard,
1492
1648
  label: 'Gemini SDK streamGenerateContent',
1493
1649
  });
1494
1650
  } finally {
@@ -1519,10 +1675,21 @@ export class GeminiProvider {
1519
1675
  });
1520
1676
  const candidate = response.candidates?.[0] || null;
1521
1677
  const textParts = candidate?.content?.parts?.filter(p => 'text' in p) ?? [];
1522
- const content = textParts.map(p => 'text' in p ? p.text : '').join('');
1523
- const toolCalls = parseToolCalls(candidate?.content?.parts ?? []);
1678
+ const rawContent = textParts.map(p => 'text' in p ? p.text : '').join('');
1679
+ const content = textLeakGuard?.enabled
1680
+ ? textLeakGuard.scrubAssistantText(rawContent)
1681
+ : rawContent;
1682
+ const leakedToolCalls = textLeakGuard?.getLeakedToolCalls() ?? [];
1683
+ let nativeToolCalls = parseToolCalls(candidate?.content?.parts ?? []);
1684
+ if (textLeakGuard?.enabled) {
1685
+ nativeToolCalls = textLeakGuard.filterNativeToolCalls(nativeToolCalls);
1686
+ }
1687
+ let toolCalls = nativeToolCalls;
1688
+ if (leakedToolCalls.length) {
1689
+ toolCalls = toolCalls?.length ? [...toolCalls, ...leakedToolCalls] : leakedToolCalls;
1690
+ }
1524
1691
  const citations = collectGeminiGroundingSources(candidate);
1525
- emitGeminiToolCalls(toolCalls, onToolCall);
1692
+ emitGeminiToolCalls(nativeToolCalls, onToolCall);
1526
1693
  // Inspect candidate.finishReason — Gemini reports terminal status here.
1527
1694
  // Only STOP (and the legacy "FINISH_REASON_STOP") plus tool/function-
1528
1695
  // call paths represent a fully delivered turn. MAX_TOKENS / SAFETY /