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
@@ -19,13 +19,16 @@ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
19
19
  import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
20
20
  import { enrichModels } from './model-catalog.mjs';
21
21
  import { makeModelCache } from './model-cache.mjs';
22
+ import { resolveAnthropicMaxTokens } from './anthropic-max-tokens.mjs';
22
23
  import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
23
24
  import {
25
+ PROVIDER_FIRST_BYTE_TIMEOUT_MS,
24
26
  PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
25
27
  PROVIDER_RETRY_BACKOFF_MS,
26
28
  PROVIDER_RETRY_MAX_ATTEMPTS,
27
- PROVIDER_SSE_IDLE_TIMEOUT_MS,
28
29
  PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
30
+ PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
31
+ streamStalledError,
29
32
  createPassthroughSignal,
30
33
  } from '../stall-policy.mjs';
31
34
  import {
@@ -38,14 +41,23 @@ import {
38
41
  withRetry,
39
42
  } from './retry-classifier.mjs';
40
43
  import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
44
+ import {
45
+ applyAnthropicEffortToBody,
46
+ effortValuesForModel,
47
+ shouldIncludeEffortBeta,
48
+ } from './anthropic-effort.mjs';
41
49
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
42
50
  import { normalizeContentForAnthropic } from './media-normalization.mjs';
43
51
  import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
52
+ import { scanLeakedToolCalls, createToolCallDedupe } from './anthropic-leaked-toolcall.mjs';
44
53
 
45
54
  // --- Model catalog cache helpers ---
46
55
  // Disk-backed cache so repeated process starts (cron, tool calls) don't
47
56
  // hammer /v1/models. 24h TTL matches the upstream client cadence.
48
57
  const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
58
+ // Bump when the on-disk cache shape changes so stale-shape entries are
59
+ // discarded instead of misread.
60
+ const ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION = 1;
49
61
  // SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
50
62
  const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
51
63
 
@@ -64,6 +76,7 @@ const SSE_MIDSTREAM_POLICY = {
64
76
  };
65
77
 
66
78
  function formatRetryAfter(ms) {
79
+ if (ms == null) return '';
67
80
  const n = Number(ms);
68
81
  if (!Number.isFinite(n) || n < 0) return '';
69
82
  if (n >= 60_000 && n % 60_000 === 0) return `${Math.round(n / 60_000)}m`;
@@ -93,6 +106,7 @@ function anthropicQuotaError(status, headers, bodyText = '') {
93
106
  const _modelCache = makeModelCache({
94
107
  fileName: 'anthropic-oauth-models.json',
95
108
  ttlMs: MODEL_CACHE_TTL_MS,
109
+ version: ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION,
96
110
  onSave: (m) => { _inMemoryCatalog = Array.isArray(m) ? m.slice() : null; },
97
111
  });
98
112
 
@@ -134,6 +148,10 @@ function _displayModel(id) {
134
148
  return `claude-${m[1].toLowerCase()}-${m[2]}${m[3] ? `.${m[3]}` : ''}`;
135
149
  }
136
150
 
151
+ function _capabilitySupported(capability) {
152
+ return capability === true || capability?.supported === true;
153
+ }
154
+
137
155
  // Classify a model id into our common tier/family shape. Anthropic's catalog
138
156
  // mixes dated ids (claude-opus-4-5-20251101), versioned aliases
139
157
  // (claude-opus-4-6), and the raw family tokens resolved via env vars.
@@ -150,15 +168,19 @@ function _normalizeAnthropicModel(raw) {
150
168
  const releaseDate = dated
151
169
  ? id.match(/-(\d{4})(\d{2})(\d{2})$/)
152
170
  : null;
171
+ const effortValues = effortValuesForModel(raw?.capabilities, id);
153
172
  return {
154
173
  id,
155
174
  display: raw?.display_name || _prettyName(id, family),
156
175
  family,
157
176
  provider: 'anthropic-oauth',
158
- contextWindow: raw?.context_window || raw?.max_context_window || _defaultContextForModel(id, family),
177
+ contextWindow: raw?.context_window || raw?.max_context_window || raw?.max_input_tokens || _defaultContextForModel(id, family),
178
+ outputTokens: raw?.max_tokens || raw?.max_output_tokens || null,
159
179
  tier,
160
180
  latest: false, // assigned in a second pass once full list is known
161
181
  releaseDate: releaseDate ? `${releaseDate[1]}-${releaseDate[2]}-${releaseDate[3]}` : null,
182
+ supportsReasoning: effortValues.length > 0 || _capabilitySupported(raw?.capabilities?.thinking),
183
+ reasoningOptions: effortValues.length ? [{ type: 'effort', values: effortValues }] : [],
162
184
  };
163
185
  }
164
186
 
@@ -201,8 +223,7 @@ function _compareVersion(a, b) {
201
223
  }
202
224
 
203
225
  // Newest HIGH-TIER chat model by version, read from the SYNC in-memory catalog
204
- // mirror. Symmetric with resolveLatestGrokModel / resolveLatestCodexModel.
205
- // Anthropic ships three families: opus / sonnet / haiku. "Latest" is the
226
+ // mirror. Anthropic ships three families: opus / sonnet / haiku. "Latest" is the
206
227
  // highest version across opus + sonnet only — haiku is the cheap tier and is
207
228
  // never the flagship default. Returns null until listModels() populates the
208
229
  // mirror; callers must warm the catalog (ensureLatestAnthropicModel) when null.
@@ -284,12 +305,11 @@ const OAUTH_SUCCESS_REDIRECT_URL = process.env.ANTHROPIC_OAUTH_SUCCESS_REDIRECT_
284
305
  const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
285
306
  const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
286
307
 
287
- // Anthropic OAuth contract for first-party OAuth clients.
288
- // Opus/Sonnet requests are gated on a specific system-prompt prefix.
289
- // Mixdog keeps that upstream client contract for OAuth routing. Haiku is not
308
+ // Anthropic OAuth contract for first-party OAuth clients: Opus/Sonnet
309
+ // requests are gated on this exact system-prompt prefix. Haiku is not
290
310
  // gated and ignores this prefix.
291
311
  const CLAUDE_CODE_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
292
- const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11,advanced-tool-use-2025-11-20';
312
+ const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11';
293
313
  const DEFAULT_CLI_VERSION = '2.1.77';
294
314
 
295
315
  function resolveCliVersion() {
@@ -298,9 +318,11 @@ function resolveCliVersion() {
298
318
  }
299
319
 
300
320
  function requiresSystemPrefix(model) {
301
- // Opus / Sonnet require the OAuth system prefix when authenticated
302
- // via OAuth. Haiku does not.
303
- return /^claude-(opus|sonnet)/i.test(String(model || ''));
321
+ // High-tier Claude OAuth models require the first-party system prefix for
322
+ // OAuth pool routing. Haiku does not; keep every other Claude family (Opus,
323
+ // Sonnet, Fable, and future non-Haiku families) on the prefixed path.
324
+ const id = String(model || '').toLowerCase();
325
+ return /^claude-/.test(id) && !/^claude-haiku(?:-|$)/.test(id);
304
326
  }
305
327
 
306
328
  // OAuth rate-limit pool routing is gated by the server inspecting the first
@@ -351,35 +373,39 @@ function buildSystemBlocks(systemMsgs, model, systemTtl, tier3Ttl) {
351
373
  return blocks;
352
374
  }
353
375
 
354
- // Per-model max_tokens when the model id is explicitly listed. New models
355
- // (e.g., Sonnet 4.7) won't match a specific entry and fall through to the
356
- // family-based heuristic below. Conservative defaultsmodel may support
357
- // more but we'd rather stay within safe bounds.
358
- const MAX_TOKENS = {
359
- 'claude-opus-4-8': 65536,
360
- 'claude-opus-4-7': 65536,
361
- 'claude-opus-4-6': 65536,
362
- 'claude-sonnet-4-6': 16384,
363
- 'claude-haiku-4-5-20251001': 8192,
364
- };
376
+ // Catalog-reported outputTokens for a model id, read from the in-memory
377
+ // catalog mirror (lazily populated from the disk cache if the mirror hasn't
378
+ // been warmed yet by listModels()). Never throwsany failure just means
379
+ // "no catalog data", and callers fall back to the static heuristics below.
380
+ function _catalogOutputTokens(model) {
381
+ if (!model) return null;
382
+ try {
383
+ if (!Array.isArray(_inMemoryCatalog)) {
384
+ const cached = _modelCache.loadSync();
385
+ if (Array.isArray(cached)) _inMemoryCatalog = cached.slice();
386
+ }
387
+ if (!Array.isArray(_inMemoryCatalog)) return null;
388
+ const entry = _inMemoryCatalog.find(m => m?.id === model);
389
+ const out = Number(entry?.outputTokens);
390
+ return Number.isFinite(out) && out > 0 ? out : null;
391
+ } catch {
392
+ return null;
393
+ }
394
+ }
365
395
 
396
+ // resolveMaxTokens: catalog-driven max_tokens for a model id. Thin wrapper
397
+ // around the shared anthropic-max-tokens helper (also used by the API-key
398
+ // twin in anthropic.mjs) — this provider supplies its own in-memory-mirror-
399
+ // first catalog lookup strategy.
400
+ // 1. MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS env override, if set, wins outright.
401
+ // 2. Catalog outputTokens (trusted over hardcoded heuristics when present),
402
+ // clamped to [MAX_TOKENS_FLOOR, safetyCap].
403
+ // 3. Static MAX_TOKENS table / family heuristic fallback when the catalog
404
+ // has no entry for this model, also clamped to the safety cap.
366
405
  function resolveMaxTokens(model) {
367
- if (MAX_TOKENS[model]) return MAX_TOKENS[model];
368
- const id = String(model || '').toLowerCase();
369
- if (id.includes('opus')) return 65536;
370
- if (id.includes('sonnet')) return 16384;
371
- if (id.includes('haiku')) return 8192;
372
- return 8192;
406
+ return resolveAnthropicMaxTokens(model, { catalogLookup: _catalogOutputTokens });
373
407
  }
374
408
 
375
- const EFFORT_BUDGET = {
376
- low: 1024,
377
- medium: 4096,
378
- high: 16384,
379
- xhigh: 32768,
380
- max: 32768,
381
- };
382
-
383
409
  const MIN_THINKING_BUDGET = 1024;
384
410
  const THINKING_OUTPUT_RESERVE = 1024;
385
411
 
@@ -392,10 +418,6 @@ function clampThinkingBudgetTokens(value, maxTokens) {
392
418
  return Math.max(MIN_THINKING_BUDGET, Math.min(desired, ceiling));
393
419
  }
394
420
 
395
- // Tracks which unknown effort labels we've already logged so a repeated
396
- // session-level misconfig doesn't flood stderr with the same warning.
397
- const _LOGGED_UNKNOWN_EFFORT = new Set();
398
-
399
421
  // Layered cache TTLs — stable layers get 1h, volatile layers get 5m.
400
422
  // Anthropic requires 1h entries to appear before 5m entries in the request.
401
423
  const CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' }; // tools, system, tier3, messages
@@ -418,8 +440,8 @@ function credentialCandidates() {
418
440
  // Fallback expiry from the access_token's JWT `exp` claim (epoch ms) when the
419
441
  // credentials file carries no explicit expiresAt — without it expiresAt stays 0,
420
442
  // which ensureAuth reads as "never expires", disabling proactive refresh. Claude
421
- // OAuth tokens are opaque so this returns 0 and the file's expiresAt governs; kept
422
- // for parity with the other OAuth providers. JWT `exp` is epoch SECONDS (RFC 7519).
443
+ // OAuth tokens are opaque so this returns 0 and the file's expiresAt governs.
444
+ // JWT `exp` is epoch SECONDS (RFC 7519).
423
445
  function _expiryFromAccessToken(token) {
424
446
  try {
425
447
  const parts = String(token || '').split('.');
@@ -644,7 +666,7 @@ export class ReauthRequired extends Error {
644
666
  }
645
667
  }
646
668
 
647
- // --- Message conversion (mirrors anthropic.mjs) ---
669
+ // --- Message conversion ---
648
670
 
649
671
  function withCacheControl(block, ttl = CACHE_TTL_VOLATILE) {
650
672
  if (!block || typeof block !== 'object' || block.cache_control) return block;
@@ -938,10 +960,23 @@ function _anthropicSseError(event) {
938
960
  return err;
939
961
  }
940
962
 
941
- async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta) {
963
+ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta, knownToolNames) {
942
964
  const reader = response.body.getReader();
943
965
  const decoder = new TextDecoder();
944
- const SSE_IDLE_TIMEOUT_MS = PROVIDER_SSE_IDLE_TIMEOUT_MS;
966
+ // SEMANTIC idle window: reset only by real model events (message/content/
967
+ // tool deltas), NOT by raw keepalive bytes. A ping-only wedge therefore
968
+ // trips this within the window instead of hanging until the 30-min agent
969
+ // watchdog. See resetIdleTimer + the per-event reset in the loop below.
970
+ // state.semanticIdleTimeoutMs is a test/override seam (same shape as
971
+ // firstMessageTimeoutMs); production uses the shared env-backed default.
972
+ const SSE_IDLE_TIMEOUT_MS = Number.isFinite(Number(state?.semanticIdleTimeoutMs))
973
+ && Number(state.semanticIdleTimeoutMs) > 0
974
+ ? Number(state.semanticIdleTimeoutMs)
975
+ : PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
976
+ const SSE_FIRST_MESSAGE_TIMEOUT_MS = Number.isFinite(Number(state?.firstMessageTimeoutMs))
977
+ && Number(state.firstMessageTimeoutMs) > 0
978
+ ? Number(state.firstMessageTimeoutMs)
979
+ : PROVIDER_FIRST_BYTE_TIMEOUT_MS;
945
980
  let content = '';
946
981
  let hasThinkingContent = false;
947
982
  const contentBlockTypes = new Set();
@@ -951,16 +986,137 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
951
986
  let stopReason = null;
952
987
  let buffer = '';
953
988
  let idleTimedOut = false;
989
+ let firstMessageTimedOut = false;
954
990
  let idleTimer = null;
991
+ let firstMessageTimer = null;
955
992
  let currentEvent = '';
956
993
 
957
994
  const pendingToolInputs = new Map();
958
995
 
996
+ // Leaked tool-call guard. The model (esp. Opus via OAuth) occasionally
997
+ // emits a tool call as plain text tags inside `text_delta` instead of a
998
+ // native `tool_use` block. `leakBuffer` is a minimal rolling window that
999
+ // only holds back text when a partial sentinel prefix is present, so a
1000
+ // tag split across chunk boundaries is still detected while ordinary text
1001
+ // still streams promptly. The guard is additive: the native tool_use path
1002
+ // (content_block_start/input_json_delta/content_block_stop) is untouched.
1003
+ const _knownTools = knownToolNames instanceof Set
1004
+ ? knownToolNames
1005
+ : new Set(Array.isArray(knownToolNames) ? knownToolNames : []);
1006
+ const _leakGuardEnabled = _knownTools.size > 0;
1007
+ const _isKnownTool = (name) => _knownTools.has(name);
1008
+ let leakBuffer = '';
1009
+ // Running markdown fence/inline-code state threaded across text_delta
1010
+ // chunks (Fix 1): a tool-call tag inside a ```code fence``` or inline span
1011
+ // is a doc example, not a real call — the guard emits it as text.
1012
+ let leakFenceState = undefined;
1013
+ // Cross-path fingerprint dedupe (Fix 2): a synthesized text-leaked call and
1014
+ // an identical native tool_use block must dispatch onToolCall exactly once.
1015
+ const _toolDedupe = createToolCallDedupe();
1016
+
1017
+ // Synthesize + dispatch a recovered leaked call exactly like the native
1018
+ // content_block_stop path (push into toolCalls, flag state, eager
1019
+ // onToolCall). A generated id uses the same `toolu_`-prefixed shape as
1020
+ // Anthropic's native tool-call ids.
1021
+ const dispatchLeakedCall = (recovered) => {
1022
+ let args = recovered?.arguments;
1023
+ if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
1024
+ // Skip if an identical native (or prior synthetic) call already fired.
1025
+ if (!_toolDedupe.shouldDispatch(recovered.name, args)) return;
1026
+ const call = {
1027
+ id: `toolu_leaked_${randomBytes(8).toString('hex')}`,
1028
+ name: recovered.name,
1029
+ arguments: args,
1030
+ };
1031
+ toolCalls.push(call);
1032
+ if (state) state.emittedToolCall = true;
1033
+ try { onToolCall?.(call); } catch {}
1034
+ try { onStreamDelta?.(); } catch {}
1035
+ };
1036
+
1037
+ // Feed accumulated text through the scanner. On `final` nothing is held
1038
+ // back so legitimate text is never lost at stream end.
1039
+ const pumpLeakBuffer = (final) => {
1040
+ if (!_leakGuardEnabled) return;
1041
+ if (!leakBuffer && !final) return;
1042
+ const { emit, calls, rest, fenceState } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final, fenceState: leakFenceState });
1043
+ leakBuffer = rest;
1044
+ leakFenceState = fenceState;
1045
+ if (emit) {
1046
+ content += emit;
1047
+ if (onTextDelta) {
1048
+ if (state) state.emittedText = true;
1049
+ try { onTextDelta(emit); } catch {}
1050
+ }
1051
+ }
1052
+ for (const c of calls) dispatchLeakedCall(c);
1053
+ };
1054
+
959
1055
  // Holds the in-flight reader.read() race rejector so the idle timer can
960
1056
  // force-unblock the loop even when reader.cancel() fails to settle the
961
1057
  // pending read (undici half-open socket). See resetIdleTimer below.
962
1058
  let idleReject = null;
963
1059
 
1060
+ const firstMessageTimeoutError = () => {
1061
+ const err = new Error(`Anthropic OAuth SSE stream produced no message_start within ${SSE_FIRST_MESSAGE_TIMEOUT_MS}ms`);
1062
+ err.code = 'EEMPTYSTREAM';
1063
+ err.isEmptyStream = true;
1064
+ err.firstByteTimeout = true;
1065
+ return err;
1066
+ };
1067
+
1068
+ const clearFirstMessageTimer = () => {
1069
+ if (firstMessageTimer) {
1070
+ clearTimeout(firstMessageTimer);
1071
+ firstMessageTimer = null;
1072
+ }
1073
+ };
1074
+
1075
+ const armFirstMessageTimer = () => {
1076
+ if (!(SSE_FIRST_MESSAGE_TIMEOUT_MS > 0)) return;
1077
+ clearFirstMessageTimer();
1078
+ firstMessageTimer = setTimeout(() => {
1079
+ if (state?.sawMessageStart) return;
1080
+ firstMessageTimedOut = true;
1081
+ const err = firstMessageTimeoutError();
1082
+ try { abortStream?.(err); } catch (abortErr) {
1083
+ try { process.stderr.write(`[anthropic-oauth] sse first-message abortStream failed: ${abortErr?.message ?? String(abortErr)}\n`); } catch {}
1084
+ }
1085
+ try {
1086
+ const _c = reader.cancel('SSE first message timeout');
1087
+ if (_c && typeof _c.catch === 'function') _c.catch(() => {});
1088
+ } catch (cancelErr) {
1089
+ try { process.stderr.write(`[anthropic-oauth] sse first-message cancel failed: ${cancelErr?.message ?? String(cancelErr)}\n`); } catch {}
1090
+ }
1091
+ if (idleReject) {
1092
+ const r = idleReject; idleReject = null; r(err);
1093
+ }
1094
+ }, SSE_FIRST_MESSAGE_TIMEOUT_MS);
1095
+ try { firstMessageTimer.unref?.(); } catch {}
1096
+ };
1097
+
1098
+ // Attach the partial stream state to a mid-stream stall error so the agent
1099
+ // loop can decide SUCCESS vs FAILURE. The recurring "worker finished but
1100
+ // owner never notified" case is a FINAL no-tool summary stream that wedges
1101
+ // ping-only after the real work (tool calls) already completed in earlier
1102
+ // iterations: there is streamed `content`, no pending tool_use, and no
1103
+ // emitted tool call this iteration. The loop treats that as a successful
1104
+ // partial-final (deliver the summary we have) instead of dropping it. A
1105
+ // stall WITH a pending/emitted tool call stays a hard failure (a tool whose
1106
+ // input never completed must never be reported as done).
1107
+ const _attachStallPartial = (err) => {
1108
+ try {
1109
+ err.partialContent = content;
1110
+ err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
1111
+ err.pendingToolUse = pendingToolInputs.size > 0;
1112
+ err.partialModel = model || undefined;
1113
+ err.partialUsage = usage;
1114
+ err.partialStopReason = stopReason || undefined;
1115
+ err.partialHasThinking = hasThinkingContent;
1116
+ } catch { /* best-effort enrichment */ }
1117
+ return err;
1118
+ };
1119
+
964
1120
  const resetIdleTimer = () => {
965
1121
  // OFF by default. When disabled the
966
1122
  // idle timer never arms, so the stream is never killed on inactivity;
@@ -983,13 +1139,14 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
983
1139
  // pending forever and the SSE idle timeout never unblocks the loop —
984
1140
  // the 391s-hang root cause.
985
1141
  if (idleReject) {
986
- const e = new Error(`Anthropic OAuth SSE stream timed out after ${SSE_IDLE_TIMEOUT_MS}ms of inactivity`);
987
- e.code = 'ETIMEDOUT';
1142
+ const e = _attachStallPartial(streamStalledError('Anthropic OAuth SSE', SSE_IDLE_TIMEOUT_MS, { emittedToolCall: !!state?.emittedToolCall }));
988
1143
  const r = idleReject; idleReject = null; r(e);
989
1144
  }
990
- // Shared provider policy: short inter-chunk inactivity catches the
991
- // sess_9cfd11-class stuck pattern where SSE starts but then goes silent.
1145
+ // Shared provider policy: short SEMANTIC-event inactivity catches the
1146
+ // ping-only wedge where SSE starts, emits some deltas, then goes silent
1147
+ // while `:ping` keepalives keep the transport socket warm.
992
1148
  }, SSE_IDLE_TIMEOUT_MS);
1149
+ try { idleTimer.unref?.(); } catch {}
993
1150
  };
994
1151
 
995
1152
  const onAbort = () => {
@@ -1009,7 +1166,13 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1009
1166
  }
1010
1167
 
1011
1168
  try {
1012
- resetIdleTimer();
1169
+ // Part A / reviewer fix: do NOT arm the SEMANTIC idle timer before the
1170
+ // stream has produced its first event. A slow first response is governed
1171
+ // by armFirstMessageTimer() (first-byte window) alone; arming the
1172
+ // semantic idle here could let it win and mis-abort a legitimately slow
1173
+ // first response as a stall. The semantic idle is first armed at
1174
+ // `message_start` (see below), so it only ever guards MID-stream silence.
1175
+ armFirstMessageTimer();
1013
1176
  streamLoop: while (true) {
1014
1177
  let chunk;
1015
1178
  try {
@@ -1021,9 +1184,10 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1021
1184
  });
1022
1185
  } catch (err) {
1023
1186
  if (idleTimedOut) {
1024
- const idleErr = new Error(`Anthropic OAuth SSE stream timed out after ${SSE_IDLE_TIMEOUT_MS}ms of inactivity`);
1025
- idleErr.code = 'ETIMEDOUT';
1026
- throw idleErr;
1187
+ throw _attachStallPartial(streamStalledError('Anthropic OAuth SSE', SSE_IDLE_TIMEOUT_MS, { emittedToolCall: !!state?.emittedToolCall }));
1188
+ }
1189
+ if (firstMessageTimedOut) {
1190
+ throw firstMessageTimeoutError();
1027
1191
  }
1028
1192
  if (signal?.aborted) {
1029
1193
  _captureMidstreamAbort(state, signal.reason);
@@ -1036,21 +1200,25 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1036
1200
  const { done, value } = chunk;
1037
1201
  if (done) break;
1038
1202
 
1039
- resetIdleTimer();
1040
1203
  buffer += decoder.decode(value, { stream: true });
1041
1204
  const lines = buffer.split('\n');
1042
1205
  buffer = lines.pop() || '';
1043
1206
 
1044
1207
  for (const line of lines) {
1045
1208
  if (line.startsWith(':')) {
1046
- // SSE comment frame (Anthropic `:ping` keepalive). The HTML Standard SSE
1047
- // spec says comments are silently ignored, but we surface them here so
1048
- // the agent stall watchdog sees the stream is still alive during Opus
1049
- // extended-thinking pauses. No content is emitted — this only refreshes
1050
- // the runtime's lastStreamDeltaAt timestamp.
1051
- try { onStreamDelta?.(); } catch {}
1209
+ // SSE comment frame (Anthropic `:ping` keepalive). Keep it
1210
+ // at transport level only: comments must not refresh the
1211
+ // agent's semantic progress timestamp, or a ping-only 200
1212
+ // can look alive forever without message_start/content.
1213
+ // Crucially it also does NOT reset the SEMANTIC idle timer
1214
+ // below a ping-only wedge must trip the idle abort.
1052
1215
  continue;
1053
1216
  }
1217
+ // Blank lines are SSE record separators — emitted after EVERY
1218
+ // frame, including `:ping` keepalives — so they are NOT semantic
1219
+ // progress and must not reset the idle timer (else a ping frame's
1220
+ // trailing blank would keep a wedge alive forever).
1221
+ if (line === '') continue;
1054
1222
  if (line.startsWith('event: ')) {
1055
1223
  currentEvent = line.slice(7).trim();
1056
1224
  continue;
@@ -1062,11 +1230,24 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1062
1230
  try {
1063
1231
  const event = JSON.parse(data);
1064
1232
 
1233
+ // SEMANTIC idle reset (Part A): reset the idle timer ONLY for
1234
+ // real progress events, NOT for Anthropic keepalives. Anthropic
1235
+ // sends pings as a NAMED event (`event: ping` /
1236
+ // `data: {"type":"ping"}`), not just `:` comment frames, so a
1237
+ // named ping must be excluded here or a ping-only wedge keeps
1238
+ // the timer alive forever. Everything that is not a ping is a
1239
+ // genuine server event (message_start/content/tool/thinking
1240
+ // deltas, message_delta/stop, errors) and counts as progress.
1241
+ if (currentEvent !== 'ping' && event?.type !== 'ping') {
1242
+ resetIdleTimer();
1243
+ }
1244
+
1065
1245
  if (currentEvent === 'error' || event?.type === 'error' || event?.error) {
1066
1246
  throw _anthropicSseError(event);
1067
1247
  }
1068
1248
 
1069
1249
  if (event.type === 'message_start' && event.message) {
1250
+ clearFirstMessageTimer();
1070
1251
  if (state) state.sawMessageStart = true;
1071
1252
  if (event.message.model) model = event.message.model;
1072
1253
  if (event.message.usage) {
@@ -1091,8 +1272,13 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1091
1272
  if (event.type === 'content_block_delta') {
1092
1273
  const delta = event.delta;
1093
1274
  if (delta?.type) contentBlockTypes.add(delta.type);
1275
+ // Time-to-first-token: stamp the first content delta
1276
+ // (text / thinking / tool input_json) exactly once so
1277
+ // the SSE trace can separate first-byte latency from
1278
+ // total stream/generation time. Without this stamp
1279
+ // ttftMs was always null and reported as 0ms.
1280
+ if (state && !state.ttftAt) state.ttftAt = Date.now();
1094
1281
  if (delta?.type === 'text_delta') {
1095
- content += delta.text || '';
1096
1282
  try { onStreamDelta?.(); } catch {}
1097
1283
  // Live text relay (gateway): forward the explicit
1098
1284
  // text chunk. thinking/signature/input_json deltas
@@ -1101,9 +1287,21 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1101
1287
  // live it cannot be withdrawn, so flag the attempt so
1102
1288
  // the mid-stream retry loop treats any later failure
1103
1289
  // as final (a retry would concatenate attempts).
1104
- if (delta.text && onTextDelta) {
1105
- if (state) state.emittedText = true;
1106
- try { onTextDelta(delta.text); } catch {}
1290
+ if (_leakGuardEnabled) {
1291
+ // Route text through the leaked-tool-call guard.
1292
+ // It appends to `content`, forwards visible text
1293
+ // via onTextDelta, and synthesizes/dispatches any
1294
+ // recovered known-tool call — suppressing the
1295
+ // tags from the visible stream. A partial sentinel
1296
+ // is held in leakBuffer until the next chunk.
1297
+ leakBuffer += delta.text || '';
1298
+ pumpLeakBuffer(false);
1299
+ } else {
1300
+ content += delta.text || '';
1301
+ if (delta.text && onTextDelta) {
1302
+ if (state) state.emittedText = true;
1303
+ try { onTextDelta(delta.text); } catch {}
1304
+ }
1107
1305
  }
1108
1306
  }
1109
1307
  if (delta?.type === 'thinking_delta' || delta?.type === 'signature_delta') {
@@ -1159,13 +1357,25 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1159
1357
  name: pending.name,
1160
1358
  arguments: parsedArgs,
1161
1359
  };
1162
- toolCalls.push(call);
1163
1360
  pendingToolInputs.delete(event.index);
1164
- if (state) state.emittedToolCall = true;
1165
1361
  // Eager dispatch: let the loop start this tool
1166
1362
  // before message_stop arrives. The loop keys
1167
1363
  // pending promises by call.id so order is safe.
1168
- try { onToolCall?.(call); } catch {}
1364
+ // Fix 2: skip the ENTIRE call (push + dispatch) when a
1365
+ // text-leaked synthetic of the same (name,args) already
1366
+ // fired — otherwise the duplicate stays in `toolCalls`
1367
+ // and the loop executes the side-effecting tool twice.
1368
+ // An invalid-args marker never fingerprint-collides with
1369
+ // a real recovered call, so malformed native calls still
1370
+ // dispatch (the marker path is unaffected).
1371
+ if (_toolDedupe.shouldDispatch(call.name, call.arguments)) {
1372
+ toolCalls.push(call);
1373
+ if (state) state.emittedToolCall = true;
1374
+ // Eager dispatch: let the loop start this tool
1375
+ // before message_stop arrives. The loop keys
1376
+ // pending promises by call.id so order is safe.
1377
+ try { onToolCall?.(call); } catch {}
1378
+ }
1169
1379
  try { onStreamDelta?.(); } catch {}
1170
1380
  }
1171
1381
  }
@@ -1206,6 +1416,12 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1206
1416
  }
1207
1417
  }
1208
1418
 
1419
+ // Stream ended: flush any held-back leaked-tool-call buffer. `final`
1420
+ // holds nothing back, so a trailing partial sentinel that never
1421
+ // resolved into a real call is surfaced as ordinary text — legitimate
1422
+ // user-visible content is never lost on the failure path.
1423
+ pumpLeakBuffer(true);
1424
+
1209
1425
  // Truncated-stream guard: if the reader loop exited (EOF or break)
1210
1426
  // after message_start but without seeing message_stop / a tool_use
1211
1427
  // stop_reason, the assistant turn was cut off mid-flight. Returning
@@ -1242,6 +1458,7 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
1242
1458
  };
1243
1459
  } finally {
1244
1460
  if (idleTimer) clearTimeout(idleTimer);
1461
+ clearFirstMessageTimer();
1245
1462
  if (signal) signal.removeEventListener('abort', onAbort);
1246
1463
  try { reader.releaseLock(); } catch (err) {
1247
1464
  try { process.stderr.write(`[anthropic-oauth] reader releaseLock failed: ${err?.message ?? String(err)}\n`); } catch {}
@@ -1365,21 +1582,13 @@ function buildRequestBody(messages, model, tools, sendOpts) {
1365
1582
  body.tools = [...nativeTools, ...toAnthropicTools([...(tools || []), ...deferredTools])];
1366
1583
  }
1367
1584
 
1368
- const thinkingBudgetTokens = Number(opts.thinkingBudgetTokens);
1369
- if (Number.isFinite(thinkingBudgetTokens) && thinkingBudgetTokens > 0) {
1370
- const budgetTokens = clampThinkingBudgetTokens(thinkingBudgetTokens, maxTokens);
1371
- if (budgetTokens) body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
1372
- } else if (opts.effort) {
1373
- if (EFFORT_BUDGET[opts.effort]) {
1374
- const budgetTokens = clampThinkingBudgetTokens(EFFORT_BUDGET[opts.effort], maxTokens);
1375
- if (budgetTokens) body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
1376
- } else if (!_LOGGED_UNKNOWN_EFFORT.has(opts.effort)) {
1377
- _LOGGED_UNKNOWN_EFFORT.add(opts.effort);
1378
- try {
1379
- process.stderr.write(`[anthropic-oauth] unknown effort=${opts.effort} ignored (known: ${Object.keys(EFFORT_BUDGET).join(',')})\n`);
1380
- } catch {}
1381
- }
1382
- }
1585
+ applyAnthropicEffortToBody(body, {
1586
+ model,
1587
+ opts,
1588
+ maxTokens,
1589
+ clampThinkingBudgetTokens,
1590
+ logTag: 'anthropic-oauth',
1591
+ });
1383
1592
 
1384
1593
  if (opts.fast === true && supportsAnthropicFastMode(model)) {
1385
1594
  body.speed = 'fast';
@@ -1530,6 +1739,20 @@ export class AnthropicOAuthProvider {
1530
1739
  if (body.speed === 'fast') {
1531
1740
  this.fastModeBetaHeaderLatched = true;
1532
1741
  }
1742
+ // advanced-tool-use-2025-11-20 beta is only needed when this request
1743
+ // actually carries deferred (defer_loading) tools — gate the header on
1744
+ // that instead of sending it unconditionally on every request.
1745
+ const hasDeferredTools = Array.isArray(body.tools)
1746
+ && body.tools.some((t) => t && t.defer_loading === true);
1747
+ // Known tool names for the leaked-tool-call guard in parseSSEStream:
1748
+ // recovered leaked calls are only synthesized when they name a tool
1749
+ // actually offered to this request (native + lowered). Derived from the
1750
+ // final request body so it matches exactly what the model was given.
1751
+ const knownToolNames = new Set(
1752
+ (Array.isArray(body.tools) ? body.tools : [])
1753
+ .map((t) => (t && typeof t.name === 'string' ? t.name : null))
1754
+ .filter(Boolean),
1755
+ );
1533
1756
  const sessionId = opts.sessionId || null;
1534
1757
  const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
1535
1758
  // Option A: no absolute wall-clock cap on streaming generation. A stream
@@ -1597,7 +1820,8 @@ export class AnthropicOAuthProvider {
1597
1820
  'anthropic-beta': buildAnthropicBetaHeaders({
1598
1821
  base: OAUTH_BETA_HEADERS,
1599
1822
  fastMode: this.fastModeBetaHeaderLatched,
1600
- toolSearch: true,
1823
+ toolSearch: hasDeferredTools,
1824
+ effort: shouldIncludeEffortBeta(useModel, opts),
1601
1825
  }),
1602
1826
  'anthropic-dangerous-direct-browser-access': 'true',
1603
1827
  'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
@@ -1758,11 +1982,12 @@ export class AnthropicOAuthProvider {
1758
1982
  const result = await parseSSEFn(
1759
1983
  response,
1760
1984
  controller.signal,
1761
- () => controller.abort(),
1985
+ (reason) => controller.abort(reason),
1762
1986
  onStreamDelta,
1763
1987
  onToolCall,
1764
1988
  midState,
1765
1989
  onTextDelta,
1990
+ knownToolNames,
1766
1991
  );
1767
1992
 
1768
1993
  const ttftMs = midState.ttftAt ? midState.ttftAt - sseStartedAt : null;
@@ -2214,3 +2439,7 @@ export async function loginOAuth() {
2214
2439
  // Lets the SSE parser be exercised in isolation against a synthetic
2215
2440
  // ReadableStream without needing a live OAuth session.
2216
2441
  export { parseSSEStream };
2442
+
2443
+ // Test-only escape hatch for scripts/tool-smoke.mjs to verify the
2444
+ // catalog-driven max-tokens resolution without duplicating its logic.
2445
+ export const _test = { resolveMaxTokens };