mixdog 0.9.147 → 0.9.148

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 (255) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +2 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  50. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  51. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +17 -11
  52. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +20 -7
  53. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +132 -42
  55. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  56. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  57. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  58. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +58 -3
  59. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  60. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  61. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  62. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  63. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  64. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  65. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  66. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  67. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  68. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  69. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  70. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  71. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  72. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  74. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  75. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  76. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  77. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  78. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  79. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  80. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  81. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +45 -8
  82. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  84. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  85. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  86. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  88. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  90. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +178 -29
  92. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  95. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  100. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  101. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  102. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  103. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  104. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  105. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  106. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  107. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  108. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  109. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  110. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  111. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  112. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  113. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  114. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  116. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  117. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  118. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  119. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  120. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  121. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  122. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  123. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  124. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  125. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  126. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  128. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  129. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  130. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  131. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  132. package/src/runtime/attachments/store.mjs +11 -3
  133. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  134. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  135. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  136. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  137. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  138. package/src/runtime/channels/lib/webhook.mjs +39 -20
  139. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  140. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  141. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  142. package/src/runtime/media/jobs.mjs +13 -0
  143. package/src/runtime/media/renditions.mjs +7 -1
  144. package/src/runtime/media/store.mjs +42 -22
  145. package/src/runtime/memory/index.mjs +4 -9
  146. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  147. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  148. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  149. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  150. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  151. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  152. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  153. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  154. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  155. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  156. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  157. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  158. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  159. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  160. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  161. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  162. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  163. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  164. package/src/runtime/memory/lib/memory.mjs +50 -12
  165. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  166. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  167. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  168. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  169. package/src/runtime/shared/background-tasks.mjs +15 -0
  170. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  171. package/src/runtime/shared/child-guardian.mjs +2 -10
  172. package/src/runtime/shared/pid-liveness.mjs +27 -0
  173. package/src/runtime/shared/runtime-root.mjs +1 -11
  174. package/src/runtime/shared/service-discovery.mjs +1 -17
  175. package/src/runtime/shared/singleton-owner.mjs +1 -11
  176. package/src/runtime/shared/staged-update.mjs +2 -12
  177. package/src/runtime/shared/tool-status.mjs +5 -1
  178. package/src/runtime/shared/tool-surface.mjs +15 -7
  179. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  180. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  181. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  182. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  183. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  184. package/src/runtime/web-search/lib/state.mjs +15 -15
  185. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  186. package/src/session-runtime/context-status.mjs +4 -12
  187. package/src/session-runtime/runtime-core.mjs +20 -1
  188. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  189. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  190. package/src/standalone/agent-tool/notify.mjs +3 -1
  191. package/src/standalone/agent-tool/spawn-flow.mjs +48 -66
  192. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  193. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  194. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  195. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  196. package/src/standalone/agent-tool.mjs +20 -55
  197. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  198. package/src/standalone/channel-client.mjs +47 -15
  199. package/src/standalone/channel-transport.mjs +44 -25
  200. package/src/standalone/daemon.mjs +31 -2
  201. package/src/standalone/hook-bus/config.mjs +25 -9
  202. package/src/standalone/hook-bus/handlers.mjs +16 -52
  203. package/src/standalone/hook-bus.mjs +29 -23
  204. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  205. package/src/standalone/session-client.mjs +86 -23
  206. package/src/standalone/session-protocol.mjs +0 -2
  207. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  208. package/src/standalone/session-runtime-host.mjs +101 -6
  209. package/src/standalone/session-runtime-record.mjs +1 -5
  210. package/src/standalone/session-runtime-worker.mjs +52 -57
  211. package/src/standalone/session-service.mjs +4 -1
  212. package/src/standalone/session-transport.mjs +56 -25
  213. package/src/tui/App.jsx +40 -49
  214. package/src/tui/app/app-view.jsx +14 -21
  215. package/src/tui/app/core-memory-picker.mjs +29 -16
  216. package/src/tui/app/create-app-pickers.mjs +15 -36
  217. package/src/tui/app/extension-pickers.mjs +60 -49
  218. package/src/tui/app/maintenance-pickers.mjs +100 -57
  219. package/src/tui/app/message-selector.mjs +6 -5
  220. package/src/tui/app/model-picker.mjs +30 -13
  221. package/src/tui/app/onboarding-steps.mjs +21 -12
  222. package/src/tui/app/panel-epoch.mjs +56 -0
  223. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  224. package/src/tui/app/panel-surface.mjs +98 -0
  225. package/src/tui/app/project-picker.mjs +30 -29
  226. package/src/tui/app/prompt-submit.mjs +192 -97
  227. package/src/tui/app/prompt-submit.test.mjs +206 -0
  228. package/src/tui/app/provider-setup-picker.mjs +74 -43
  229. package/src/tui/app/resume-picker.mjs +11 -8
  230. package/src/tui/app/route-pickers.mjs +45 -28
  231. package/src/tui/app/settings-picker.mjs +69 -18
  232. package/src/tui/app/shell-layout.mjs +1 -3
  233. package/src/tui/app/slash-commands.mjs +0 -2
  234. package/src/tui/app/slash-dispatch.mjs +4 -2
  235. package/src/tui/app/text-entry-policy.mjs +25 -0
  236. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  237. package/src/tui/app/usage-context-panels.mjs +21 -25
  238. package/src/tui/app/use-global-key-input.mjs +3 -2
  239. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  240. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  241. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  242. package/src/tui/components/Picker.jsx +1 -1
  243. package/src/tui/components/TextEntryPanel.jsx +7 -2
  244. package/src/tui/dist/index.mjs +652 -561
  245. package/src/tui/lib/voice-setup.mjs +1 -2
  246. package/src/tui/session/agent-job-feed.mjs +73 -11
  247. package/src/tui/session/session-api-ext.mjs +5 -6
  248. package/src/tui/session/session-api.mjs +1 -0
  249. package/src/tui/session/tool-approval.mjs +15 -5
  250. package/src/tui/session/tui-steering-persist.mjs +33 -19
  251. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  252. package/src/tui/session-local.mjs +0 -22
  253. package/scripts/.tmp-coverage-report.mjs +0 -96
  254. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  255. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -377,12 +377,28 @@ export async function executeMcpTool(name, args, options = {}) {
377
377
  if (!server)
378
378
  throw new Error(`MCP server "${serverName}" not connected`);
379
379
  const gate = callAdmissionFor(server);
380
+ const callSignal = options?.signal || null;
380
381
  return gate.run(options?.ownerKey || currentToolExecutionOwner(), async () => {
382
+ // The gate stops forwarding the caller's abort once this task is
383
+ // admitted, so the task itself observes the signal for its whole run.
384
+ if (callSignal?.aborted) {
385
+ throw mcpAbortError(
386
+ callSignal,
387
+ `MCP tool call aborted before dispatch (server="${serverName}", tool="${toolName}")`,
388
+ );
389
+ }
381
390
  let result;
382
391
  try {
383
- result = await _callToolWithTimeout(server, toolName, args);
392
+ result = await _callToolWithTimeout(server, toolName, args, callSignal);
384
393
  } catch (firstErr) {
385
394
  const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
395
+ if (isMcpCallAbortError(firstErr) || callSignal?.aborted) {
396
+ // A cancelled call is not a transport failure: reconnecting and
397
+ // replaying it would duplicate the side effect the caller just
398
+ // walked away from.
399
+ mcpLog(`[mcp-client] Tool call aborted by caller for "${serverName}/${toolName}".\n`);
400
+ throw firstErr;
401
+ }
386
402
  if (isMcpToolCallTimeoutError(firstErr)) {
387
403
  mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
388
404
  throw firstErr;
@@ -390,14 +406,33 @@ export async function executeMcpTool(name, args, options = {}) {
390
406
  mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
391
407
  let retryServer;
392
408
  try {
393
- retryServer = await reconnectMcpServer(scopeId, serverName, server);
409
+ // The reconnect is SHARED (singleflight): the race only stops
410
+ // THIS caller from waiting on it — the reconnect keeps running
411
+ // for its other waiters. Without this, an abort mid-reconnect
412
+ // could not settle the call and the admission slot stayed held
413
+ // until the reconnect finished, or forever if it hung.
414
+ retryServer = await raceMcpAbort(
415
+ reconnectMcpServer(scopeId, serverName, server),
416
+ callSignal,
417
+ `MCP tool call aborted during reconnect (server="${serverName}", tool="${toolName}")`,
418
+ );
394
419
  } catch (reconnectErr) {
420
+ if (isMcpCallAbortError(reconnectErr) || callSignal?.aborted) throw reconnectErr;
395
421
  const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
396
422
  throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
397
423
  }
424
+ // A reconnect that outlived the caller's abort must not dispatch the
425
+ // retry: that would run the side effect after cancellation.
426
+ if (callSignal?.aborted) {
427
+ throw mcpAbortError(
428
+ callSignal,
429
+ `MCP tool call aborted after reconnect (server="${serverName}", tool="${toolName}")`,
430
+ );
431
+ }
398
432
  try {
399
- result = await _callToolWithTimeout(retryServer, toolName, args);
433
+ result = await _callToolWithTimeout(retryServer, toolName, args, callSignal);
400
434
  } catch (retryErr) {
435
+ if (isMcpCallAbortError(retryErr) || callSignal?.aborted) throw retryErr;
401
436
  const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
402
437
  throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
403
438
  }
@@ -408,7 +443,7 @@ export async function executeMcpTool(name, args, options = {}) {
408
443
  ? makeToolEnvelope(text, [], { explicitSuccess: true })
409
444
  : text;
410
445
  }, {
411
- signal: options?.signal || null,
446
+ signal: callSignal,
412
447
  });
413
448
  }
414
449
 
@@ -471,11 +506,58 @@ export function resolveMcpStartupTimeoutMs(cfg = {}, env = process.env) {
471
506
  return Math.round(parsed);
472
507
  }
473
508
 
474
- async function _callToolWithTimeout(server, toolName, args) {
509
+ // Caller-driven cancellation (hook timeout, aborted turn, closed pane).
510
+ // Admission alone is not cancellation: an admitted call that ignores the
511
+ // caller's signal keeps its concurrency slot until the SERVER timeout, which an
512
+ // operator may have disabled entirely.
513
+ function mcpAbortError(signal, message) {
514
+ const reason = signal?.reason;
515
+ if (reason instanceof Error) return reason;
516
+ const error = new Error(reason ? String(reason) : message);
517
+ error.code = 'EMCPCALLABORTED';
518
+ return error;
519
+ }
520
+
521
+ function isMcpCallAbortError(err) {
522
+ return err?.code === 'EMCPCALLABORTED' || err?.name === 'AbortError';
523
+ }
524
+
525
+ /** Settle as soon as `signal` aborts. Promise.race keeps a later rejection of
526
+ * `promise` handled, so the abandoned call cannot surface as an unhandled
527
+ * rejection. */
528
+ function raceMcpAbort(promise, signal, message) {
529
+ if (!signal) return promise;
530
+ if (signal.aborted) {
531
+ void Promise.resolve(promise).catch(() => {});
532
+ return Promise.reject(mcpAbortError(signal, message));
533
+ }
534
+ let onAbort = null;
535
+ const aborted = new Promise((_resolve, reject) => {
536
+ onAbort = () => reject(mcpAbortError(signal, message));
537
+ signal.addEventListener('abort', onAbort, { once: true });
538
+ });
539
+ return Promise.race([promise, aborted]).finally(() => {
540
+ if (onAbort) {
541
+ try { signal.removeEventListener('abort', onAbort); } catch { /* already detached */ }
542
+ }
543
+ });
544
+ }
545
+
546
+ async function _callToolWithTimeout(server, toolName, args, signal = null) {
475
547
  let timer;
476
548
  const timeoutMs = resolveMcpCallTimeoutMs(server?.cfg);
549
+ // The signal goes INTO the SDK request: an aborted caller cancels the live
550
+ // JSON-RPC request (notifications/cancelled) instead of merely walking away
551
+ // from it. The race below additionally frees the admission slot at once.
552
+ const requestOptions = signal ? { signal } : undefined;
553
+ const abortMessage = `MCP tool call aborted (server="${server?.name}", tool="${toolName}")`;
477
554
  if (!(timeoutMs > 0)) {
478
- return server.client.callTool({ name: toolName, arguments: args });
555
+ // Server timeout disabled: the caller's signal is then the ONLY bound.
556
+ return raceMcpAbort(
557
+ server.client.callTool({ name: toolName, arguments: args }, undefined, requestOptions),
558
+ signal,
559
+ abortMessage,
560
+ );
479
561
  }
480
562
  const timeout = new Promise((_, rej) => {
481
563
  timer = setTimeout(() => {
@@ -492,10 +574,10 @@ async function _callToolWithTimeout(server, toolName, args) {
492
574
  if (timer.unref) timer.unref();
493
575
  });
494
576
  try {
495
- return await Promise.race([
496
- server.client.callTool({ name: toolName, arguments: args }),
577
+ return await raceMcpAbort(Promise.race([
578
+ server.client.callTool({ name: toolName, arguments: args }, undefined, requestOptions),
497
579
  timeout,
498
- ]);
580
+ ]), signal, abortMessage);
499
581
  } finally {
500
582
  if (timer) clearTimeout(timer);
501
583
  }
@@ -878,7 +960,9 @@ export function _registerMcpServerForTest(scopeId, name, rawTools = [], options
878
960
  callTool: typeof options.callTool === 'function'
879
961
  ? options.callTool
880
962
  : async () => ({ content: [{ type: 'text', text: 'ok' }] }),
881
- close: async () => {},
963
+ // Injectable so a test can stall teardown (and therefore the shared
964
+ // reconnect) without a transport or a child process.
965
+ close: typeof options.close === 'function' ? options.close : async () => {},
882
966
  },
883
967
  generation: currentConnectAbortGeneration(normalizedScopeId),
884
968
  };
@@ -4,6 +4,7 @@ const EXTENDED_CACHE_TTL_BETA_HEADER = 'extended-cache-ttl-2025-04-11';
4
4
  const INTERLEAVED_THINKING_BETA_HEADER = 'interleaved-thinking-2025-05-14';
5
5
  const FAST_MODE_BETA_HEADER = 'fast-mode-2026-02-01';
6
6
  const TOOL_SEARCH_BETA_HEADER = 'advanced-tool-use-2025-11-20';
7
+ export const SERVER_SIDE_FALLBACK_BETA_HEADER = 'server-side-fallback-2026-07-01';
7
8
  export { EFFORT_BETA_HEADER };
8
9
 
9
10
  export function supportsAnthropicFastMode(model) {
@@ -16,6 +17,7 @@ export function buildAnthropicBetaHeaders({
16
17
  fastMode = false,
17
18
  toolSearch = false,
18
19
  effort = false,
20
+ serverFallback = false,
19
21
  } = {}) {
20
22
  const headers = String(base || '')
21
23
  .split(',')
@@ -30,5 +32,8 @@ export function buildAnthropicBetaHeaders({
30
32
  if (effort && !headers.includes(EFFORT_BETA_HEADER)) {
31
33
  headers.push(EFFORT_BETA_HEADER);
32
34
  }
35
+ if (serverFallback && !headers.includes(SERVER_SIDE_FALLBACK_BETA_HEADER)) {
36
+ headers.push(SERVER_SIDE_FALLBACK_BETA_HEADER);
37
+ }
33
38
  return headers.join(',');
34
39
  }
@@ -0,0 +1,98 @@
1
+ // Fast-mode capacity policy for the Anthropic providers.
2
+ //
3
+ // `speed: 'fast'` rides a separate capacity pool. When that pool is exhausted
4
+ // the request comes back 429 (rate limit) or 529 (overloaded) — waiting out a
5
+ // long Retry-After on the fast pool wastes the turn AND thrashes the prompt
6
+ // cache, because the retry after the wait carries a different speed anyway.
7
+ // The reference client's rule: a SHORT server window is worth waiting for at
8
+ // fast speed (the cache prefix survives), anything longer drops to standard
9
+ // speed for a cooldown. An explicit "overage billing unavailable" header is
10
+ // terminal for the process — no cooldown can make paid overage appear.
11
+ //
12
+ // State is process-wide because the capacity pool is per-account, not per
13
+ // session: one session discovering the pool is empty must not leave sibling
14
+ // sessions hammering it.
15
+
16
+ // Server-declared reason header for a fast-mode rejection caused by missing
17
+ // overage billing (matches the reference client's header name).
18
+ const OVERAGE_DISABLED_HEADER = 'anthropic-ratelimit-unified-overage-disabled-reason';
19
+
20
+ // A Retry-After under this bound is short enough that waiting keeps fast mode
21
+ // (and its cache prefix) worthwhile.
22
+ const SHORT_RETRY_THRESHOLD_MS = 10_000;
23
+ // Cooldown floor: without it a 1s window would flip fast mode on and off every
24
+ // turn, which costs more in cache misses than the downgrade saves.
25
+ const MIN_COOLDOWN_MS = 60_000;
26
+ // Applied when the server gives no window at all.
27
+ const DEFAULT_COOLDOWN_MS = 300_000;
28
+
29
+ let _cooldownUntilMs = 0;
30
+ let _disabledReason = null;
31
+
32
+ function _headerValue(headers, name) {
33
+ if (!headers) return null;
34
+ const lower = name.toLowerCase();
35
+ if (typeof headers.get === 'function') return headers.get(name) ?? headers.get(lower);
36
+ for (const [key, value] of Object.entries(headers)) {
37
+ if (String(key).toLowerCase() === lower) return Array.isArray(value) ? value[0] : value;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ /** True when a request may still ask for `speed: 'fast'`. */
43
+ export function fastModeAvailable(now = Date.now()) {
44
+ return _disabledReason === null && now >= _cooldownUntilMs;
45
+ }
46
+
47
+ export function fastModeCooldownRemainingMs(now = Date.now()) {
48
+ if (_disabledReason !== null) return Number.POSITIVE_INFINITY;
49
+ return Math.max(0, _cooldownUntilMs - now);
50
+ }
51
+
52
+ export function fastModeDisabledReason() {
53
+ return _disabledReason;
54
+ }
55
+
56
+ /**
57
+ * Record a capacity failure observed on a fast-mode request.
58
+ *
59
+ * Returns the decision for the caller:
60
+ * 'ignored' — not a fast request or not a capacity status; nothing changed.
61
+ * 'retry-fast'— short server window; keep fast mode and let the normal retry
62
+ * path wait it out.
63
+ * 'downgrade' — cooldown started; the next attempt must drop `speed`.
64
+ * 'disabled' — overage unavailable; fast mode is off for this process.
65
+ */
66
+ export function noteFastModeCapacityError(err, { fast = false, now = Date.now() } = {}) {
67
+ if (!fast) return 'ignored';
68
+ const status = Number(err?.httpStatus || err?.status || err?.response?.status || 0);
69
+ if (status !== 429 && status !== 529) return 'ignored';
70
+
71
+ const headers = err?.headers || err?.response?.headers || err?.data?.responseHeaders || null;
72
+ const overageReason = _headerValue(headers, OVERAGE_DISABLED_HEADER);
73
+ if (overageReason != null && String(overageReason) !== '') {
74
+ _disabledReason = String(overageReason);
75
+ return 'disabled';
76
+ }
77
+
78
+ const retryAfterMs = Number(err?.retryAfterMs);
79
+ const hasWindow = Number.isFinite(retryAfterMs) && retryAfterMs >= 0;
80
+ if (hasWindow && retryAfterMs < SHORT_RETRY_THRESHOLD_MS) return 'retry-fast';
81
+
82
+ const cooldownMs = Math.max(hasWindow ? retryAfterMs : DEFAULT_COOLDOWN_MS, MIN_COOLDOWN_MS);
83
+ _cooldownUntilMs = Math.max(_cooldownUntilMs, now + cooldownMs);
84
+ return 'downgrade';
85
+ }
86
+
87
+ /** Manual re-enable (user action) and test seam. */
88
+ export function clearFastModeCooldown() {
89
+ _cooldownUntilMs = 0;
90
+ _disabledReason = null;
91
+ }
92
+
93
+ export const _FAST_MODE_POLICY = Object.freeze({
94
+ OVERAGE_DISABLED_HEADER,
95
+ SHORT_RETRY_THRESHOLD_MS,
96
+ MIN_COOLDOWN_MS,
97
+ DEFAULT_COOLDOWN_MS,
98
+ });
@@ -0,0 +1,90 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ _FAST_MODE_POLICY,
6
+ clearFastModeCooldown,
7
+ fastModeAvailable,
8
+ fastModeCooldownRemainingMs,
9
+ fastModeDisabledReason,
10
+ noteFastModeCapacityError,
11
+ } from './anthropic-fast-mode.mjs';
12
+
13
+ test.beforeEach(() => clearFastModeCooldown());
14
+ test.after(() => clearFastModeCooldown());
15
+
16
+ test('non-capacity failures and standard-speed requests leave fast mode alone', () => {
17
+ assert.equal(noteFastModeCapacityError({ httpStatus: 429 }, { fast: false }), 'ignored');
18
+ assert.equal(noteFastModeCapacityError({ httpStatus: 500 }, { fast: true }), 'ignored');
19
+ assert.equal(noteFastModeCapacityError({ httpStatus: 401 }, { fast: true }), 'ignored');
20
+ assert.equal(fastModeAvailable(), true);
21
+ });
22
+
23
+ test('a short server window keeps fast mode so the cache prefix survives', () => {
24
+ const decision = noteFastModeCapacityError(
25
+ { httpStatus: 429, retryAfterMs: _FAST_MODE_POLICY.SHORT_RETRY_THRESHOLD_MS - 1 },
26
+ { fast: true },
27
+ );
28
+ assert.equal(decision, 'retry-fast');
29
+ assert.equal(fastModeAvailable(), true);
30
+ });
31
+
32
+ test('a long window downgrades to standard speed for at least the cooldown floor', () => {
33
+ const now = 1_000_000;
34
+ const retryAfterMs = 20_000;
35
+ assert.equal(
36
+ noteFastModeCapacityError({ httpStatus: 429, retryAfterMs }, { fast: true, now }),
37
+ 'downgrade',
38
+ );
39
+ assert.equal(fastModeAvailable(now), false);
40
+ // 20s window is below the floor, so the floor wins.
41
+ assert.equal(fastModeCooldownRemainingMs(now), _FAST_MODE_POLICY.MIN_COOLDOWN_MS);
42
+ assert.equal(fastModeAvailable(now + _FAST_MODE_POLICY.MIN_COOLDOWN_MS), true);
43
+ });
44
+
45
+ test('a long window longer than the floor is honored verbatim', () => {
46
+ const now = 2_000_000;
47
+ const retryAfterMs = _FAST_MODE_POLICY.MIN_COOLDOWN_MS * 3;
48
+ noteFastModeCapacityError({ httpStatus: 529, retryAfterMs }, { fast: true, now });
49
+ assert.equal(fastModeCooldownRemainingMs(now), retryAfterMs);
50
+ });
51
+
52
+ test('a 529 with no window uses the default cooldown', () => {
53
+ const now = 3_000_000;
54
+ assert.equal(noteFastModeCapacityError({ httpStatus: 529 }, { fast: true, now }), 'downgrade');
55
+ assert.equal(fastModeCooldownRemainingMs(now), _FAST_MODE_POLICY.DEFAULT_COOLDOWN_MS);
56
+ });
57
+
58
+ test('cooldowns never shrink an existing longer cooldown', () => {
59
+ const now = 4_000_000;
60
+ noteFastModeCapacityError({ httpStatus: 429, retryAfterMs: 600_000 }, { fast: true, now });
61
+ noteFastModeCapacityError({ httpStatus: 429, retryAfterMs: 60_000 }, { fast: true, now });
62
+ assert.equal(fastModeCooldownRemainingMs(now), 600_000);
63
+ });
64
+
65
+ test('an overage-disabled header turns fast mode off for the process', () => {
66
+ const decision = noteFastModeCapacityError(
67
+ {
68
+ httpStatus: 429,
69
+ headers: new Map([[_FAST_MODE_POLICY.OVERAGE_DISABLED_HEADER, 'no_overage']]),
70
+ retryAfterMs: 1,
71
+ },
72
+ { fast: true },
73
+ );
74
+ assert.equal(decision, 'disabled');
75
+ assert.equal(fastModeDisabledReason(), 'no_overage');
76
+ assert.equal(fastModeAvailable(Date.now() + 86_400_000), false);
77
+ clearFastModeCooldown();
78
+ assert.equal(fastModeAvailable(), true);
79
+ });
80
+
81
+ test('plain-object headers resolve case-insensitively', () => {
82
+ noteFastModeCapacityError(
83
+ {
84
+ httpStatus: 429,
85
+ response: { headers: { 'ANTHROPIC-RateLimit-Unified-Overage-Disabled-Reason': 'billing' } },
86
+ },
87
+ { fast: true },
88
+ );
89
+ assert.equal(fastModeDisabledReason(), 'billing');
90
+ });
@@ -394,14 +394,43 @@ export function toolCallFingerprint(name, args) {
394
394
  if (a === null || typeof a !== 'object' || Array.isArray(a)) a = {};
395
395
  return traceHash(stableTraceStringify({ name: name || '', args: a }));
396
396
  }
397
+ // Ids minted locally when a call is recovered from text (`call_leaked_…`,
398
+ // `toolu_leaked_…`, `gemini_leaked_…`). Such an id is not a server identity, so
399
+ // a synthetic call can only be identified by its name+args fingerprint —
400
+ // whereas a provider-assigned id IS the identity of a real call.
401
+ export function isLeakedSyntheticToolCallId(id) {
402
+ return typeof id === 'string' && id.includes('_leaked_');
403
+ }
397
404
  export function createToolCallDedupe() {
398
405
  const seen = new Set();
406
+ const leaked = new Set();
407
+ const dispatchedIds = new Set();
399
408
  return {
400
- // True the first time this (name,args) fingerprint is seen; false on
401
- // any later identical call (synthetic-then-native or vice-versa).
402
- shouldDispatch(name, args) {
409
+ // True the first time this call should be dispatched.
410
+ //
411
+ // Identity depends on what the provider gave us:
412
+ // - no id / a locally minted `_leaked_` id → the (name,args)
413
+ // fingerprint, because that is all a text-recovered call has, and
414
+ // - a provider-assigned id → the id itself. Parallel tool calls are
415
+ // emitted with identical name+args and DIFFERENT ids; each one owns
416
+ // a tool_result the next request must carry, so collapsing them by
417
+ // fingerprint drops results and breaks the response chain.
418
+ // The cross-path guard survives: a native call whose fingerprint a
419
+ // synthetic leak already dispatched is still suppressed (once).
420
+ shouldDispatch(name, args, id = '') {
403
421
  const fp = toolCallFingerprint(name, args);
404
- if (seen.has(fp)) return false;
422
+ const callId = typeof id === 'string' ? id : '';
423
+ if (!callId || isLeakedSyntheticToolCallId(callId)) {
424
+ if (seen.has(fp)) return false;
425
+ seen.add(fp);
426
+ leaked.add(fp);
427
+ return true;
428
+ }
429
+ if (dispatchedIds.has(callId)) return false;
430
+ dispatchedIds.add(callId);
431
+ // Claim (consume) one pending leak of the same shape: this native
432
+ // frame is that leak surfacing on the native path.
433
+ if (leaked.delete(fp)) return false;
405
434
  seen.add(fp);
406
435
  return true;
407
436
  },
@@ -410,26 +439,48 @@ export function createToolCallDedupe() {
410
439
  }
411
440
 
412
441
  /**
413
- * Drop duplicate tool calls from a RETURNED `toolCalls` array by name+args
414
- * fingerprint (Fix 2, array side). Dispatch-time dedupe (`createToolCallDedupe`)
415
- * only suppresses the second `onToolCall`; providers that also RETURN a
416
- * `toolCalls` array (which the agent loop executes) can still carry a
417
- * synthetic-leaked + identical-native duplicate. Run the final array through
418
- * this so the loop never executes a side-effecting tool twice. Order is
419
- * preserved; the FIRST occurrence of each fingerprint wins.
442
+ * Drop duplicate tool calls from a RETURNED `toolCalls` array (Fix 2, array
443
+ * side). Dispatch-time dedupe (`createToolCallDedupe`) only suppresses the
444
+ * second `onToolCall`; providers that also RETURN a `toolCalls` array (which
445
+ * the agent loop executes) can still carry a synthetic-leaked + identical-native
446
+ * duplicate. Run the final array through this so the loop never executes a
447
+ * side-effecting tool twice.
420
448
  *
421
- * @param {Array<{name?:string,arguments?:object}>} calls
422
- * @returns {Array} the array with later duplicate-fingerprint calls removed.
449
+ * Identity mirrors createToolCallDedupe: a provider-assigned id identifies a
450
+ * real call, so parallel calls sharing a name+args fingerprint but carrying
451
+ * distinct ids are ALL kept — collapsing them would leave the model waiting for
452
+ * tool_results that never arrive and break the next request's chain. Only a
453
+ * repeated id, a synthetic leak the native path already carries, or an id-less
454
+ * exact repeat is dropped. Order is preserved; the FIRST occurrence wins.
455
+ *
456
+ * @param {Array<{id?:string,name?:string,arguments?:object}>} calls
457
+ * @returns {Array} the array with genuine duplicates removed.
423
458
  */
424
459
  export function dedupeToolCallList(calls) {
425
460
  if (!Array.isArray(calls) || calls.length < 2) return calls;
426
- const seen = new Set();
461
+ // Fingerprints that reached the array through a provider-assigned id: a
462
+ // synthetic leak matching one of these is the same call, recovered twice.
463
+ const nativeFingerprints = new Set();
464
+ for (const call of calls) {
465
+ if (!call || typeof call !== 'object') continue;
466
+ if (!call.id || isLeakedSyntheticToolCallId(call.id)) continue;
467
+ nativeFingerprints.add(toolCallFingerprint(call.name, call.arguments));
468
+ }
469
+ const seenIds = new Set();
470
+ const seenSyntheticFingerprints = new Set();
427
471
  const out = [];
428
472
  for (const call of calls) {
429
473
  if (!call || typeof call !== 'object') { out.push(call); continue; }
474
+ const id = typeof call.id === 'string' ? call.id : '';
475
+ if (id && !isLeakedSyntheticToolCallId(id)) {
476
+ if (seenIds.has(id)) continue;
477
+ seenIds.add(id);
478
+ out.push(call);
479
+ continue;
480
+ }
430
481
  const fp = toolCallFingerprint(call.name, call.arguments);
431
- if (seen.has(fp)) continue;
432
- seen.add(fp);
482
+ if (nativeFingerprints.has(fp) || seenSyntheticFingerprints.has(fp)) continue;
483
+ seenSyntheticFingerprints.add(fp);
433
484
  out.push(call);
434
485
  }
435
486
  return out.length === calls.length ? calls : out;
@@ -1,7 +1,6 @@
1
1
  // Anthropic model catalog + message conversion helpers, extracted from anthropic.mjs.
2
2
  import { createRequire } from 'node:module';
3
3
  import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
4
- import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
5
4
  import {
6
5
  ANTHROPIC_RETRY_BACKOFF_MS,
7
6
  ANTHROPIC_RETRY_JITTER_RATIO,
@@ -32,7 +31,6 @@ import {
32
31
  effortValuesForModel,
33
32
  shouldIncludeEffortBeta,
34
33
  } from './anthropic-effort.mjs';
35
- import { normalizeContentForAnthropic } from './media-normalization.mjs';
36
34
  import { enrichModels } from './model-catalog.mjs';
37
35
  import { sanitizeModelList } from './model-list-sanitize.mjs';
38
36
  import { makeModelCache } from './model-cache.mjs';
@@ -49,8 +47,12 @@ import {
49
47
  normalizeAnthropicNonStreamingResponse,
50
48
  resolveAnthropicCacheTtls as resolveCacheTtls,
51
49
  sanitizeAnthropicInputSchema,
50
+ toAnthropicMessages,
52
51
  toAnthropicToolChoice,
53
52
  } from './lib/anthropic-request-utils.mjs';
53
+ // Message lowering lives in the shared request-utils lib (one implementation
54
+ // for both Anthropic providers); re-exported here for existing importers.
55
+ export { toAnthropicMessages };
54
56
 
55
57
 
56
58
  export const require = createRequire(import.meta.url);
@@ -235,86 +237,6 @@ export function deferredAnthropicTools(activeTools, messages, opts) {
235
237
  export function requestAnthropicTools(tools, messages, opts) {
236
238
  return sharedRequestAnthropicTools(tools, messages, opts, 'anthropic');
237
239
  }
238
- export function toAnthropicMessages(messages) {
239
- // Marker-free lowering. cache_control is applied AFTER sanitization by
240
- // applyAnthropicCacheMarkers() so that block drops/inserts/reorders
241
- // performed by sanitizeAnthropicContentPairs cannot move or delete a
242
- // marked block (the root cause of the sporadic COLD-turn cache miss:
243
- // pre-sanitize markers landed on blocks the sanitizer then rewrote, so
244
- // the provider-visible breakpoint diverged from the cached one).
245
- const result = [];
246
- for (let idx = 0; idx < messages.length; idx++) {
247
- const m = messages[idx];
248
- if (m.role === 'system') continue;
249
- if (m.role === 'assistant' && (m.toolCalls?.length || m.assistantBlocks?.length || m.thinkingBlocks?.length)) {
250
- let content;
251
- if (m.assistantBlocks?.length) {
252
- content = m.assistantBlocks.slice();
253
- } else {
254
- content = [];
255
- // Adaptive-thinking round-trip: prior-turn thinking blocks are
256
- // REQUIRED back, unmodified (signature intact; empty thinking
257
- // field allowed), and MUST precede tool_use blocks.
258
- if (Array.isArray(m.thinkingBlocks)) {
259
- for (const tb of m.thinkingBlocks) {
260
- if (tb && typeof tb === 'object') content.push(tb);
261
- }
262
- }
263
- if (m.content) content.push({ type: 'text', text: m.content });
264
- for (const tc of m.toolCalls || []) {
265
- content.push({
266
- type: 'tool_use',
267
- id: tc.id,
268
- name: tc.name,
269
- input: tc.arguments,
270
- });
271
- }
272
- }
273
- result.push({ role: 'assistant', content });
274
- continue;
275
- }
276
- if (m.role === 'tool') {
277
- const last = result[result.length - 1];
278
- const native = m.nativeToolSearch;
279
- const nativeProvider = String(native?.provider || '').toLowerCase();
280
- const anthropicNative = new Set(['anthropic', 'anthropic-oauth']);
281
- const references = (!nativeProvider || anthropicNative.has(nativeProvider))
282
- && Array.isArray(native?.toolReferences)
283
- ? native.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
284
- : [];
285
- const block = {
286
- type: 'tool_result',
287
- tool_use_id: m.toolCallId || '',
288
- content: references.length
289
- ? references.map((tool_name) => ({ type: 'tool_reference', tool_name }))
290
- : normalizeContentForAnthropic(m.content),
291
- ...((m.toolKind === 'error' || m.isError === true) ? { is_error: true } : {}),
292
- };
293
- if (last?.role === 'user' && Array.isArray(last.content)) {
294
- last.content.push(block);
295
- }
296
- else {
297
- result.push({ role: 'user', content: [block] });
298
- }
299
- continue;
300
- }
301
- // First-party client parity: fold a user text turn that directly follows a
302
- // tool_result turn into that tool_result's content (empty end_turn
303
- // livelock prevention; see foldUserTextIntoToolResultTail).
304
- // EXCEPTION: steering-origin user messages (human/TUI interjections)
305
- // keep their own user turn so provenance survives — folding them would
306
- // disguise user input as tool output. Anthropic accepts a user text
307
- // message after a tool_result message, so the turn stays request-valid.
308
- const isSteering = m.role === 'user' && m.meta?.source === 'steering';
309
- if (m.role === 'user' && !isSteering
310
- && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
311
- continue;
312
- }
313
- result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
314
- }
315
- return sanitizeAnthropicContentPairs(result);
316
- }
317
-
318
240
  // Test-only: expose the lowering so the steering-provenance test can assert
319
241
  // the API-key provider keeps steering-tagged user turns distinct (mirrors
320
242
  // anthropic-oauth._buildRequestBodyForCacheSmoke coverage).
@@ -115,11 +115,6 @@ function _loadCredentialsFile(path) {
115
115
  }
116
116
  }
117
117
 
118
- export function loadCredentialsFromPath(path) {
119
- if (!path) return null;
120
- return _loadCredentialsFile(resolve(path));
121
- }
122
-
123
118
  // Cross-process safe credential save. Lockfile (O_EXCL) prevents two Mixdog
124
119
  // refreshers from clobbering each other; atomic rename guarantees readers see
125
120
  // either the old or new file, never a half-written one. Used so refresh_token