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.
- package/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +2 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +17 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +20 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +132 -42
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +58 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +45 -8
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +178 -29
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +48 -66
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
401
|
-
//
|
|
402
|
-
|
|
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
|
-
|
|
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
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
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
|
-
*
|
|
422
|
-
*
|
|
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
|
-
|
|
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 (
|
|
432
|
-
|
|
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
|