mixdog 0.9.94 → 0.9.96
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +109 -0
- package/package.json +26 -13
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +20 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +13 -2
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +29 -14
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +29 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +13 -14
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +82 -22
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +40 -36
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -33
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +168 -83
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +34 -18
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +89 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +6 -39
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -6
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -4
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +124 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +7 -16
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +20 -43
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +76 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +38 -13
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +138 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +177 -16
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +21 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +217 -25
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +60 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/config.mjs +2 -2
- package/src/runtime/channels/lib/inbound-handler.mjs +1 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +3 -3
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/index.mjs +5 -38
- package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +8 -0
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/tool-defs.mjs +7 -11
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +133 -4
- package/src/runtime/shared/background-tasks.mjs +6 -3
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +60 -31
- package/src/runtime/shared/config.mjs +130 -44
- package/src/runtime/shared/resource-admission.mjs +146 -58
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +2 -7
- package/src/runtime/shared/turn-snapshot.mjs +411 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +552 -0
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/runtime-core.mjs +57 -19
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +11 -2
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +139 -23
- package/src/session-runtime/settings-api.mjs +5 -17
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +25 -28
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +631 -0
- package/src/standalone/channel-admin.mjs +12 -12
- package/src/standalone/channel-daemon-client.mjs +17 -1
- package/src/standalone/channel-daemon-transport.mjs +231 -9
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +1069 -0
- package/src/standalone/engine-daemon-protocol.mjs +32 -0
- package/src/standalone/engine-daemon-service.mjs +1108 -0
- package/src/standalone/engine-daemon-transport.mjs +794 -0
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/session-protocol.mjs +188 -0
- package/src/tui/App.jsx +92 -64
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +10 -2
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +20 -20
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +24 -20
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +59 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +16 -9
- package/src/tui/app/settings-picker.mjs +60 -65
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +57 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +39 -17
- package/src/tui/app/use-transcript-scroll.mjs +39 -10
- package/src/tui/app/use-transcript-window.mjs +62 -14
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +81 -38
- package/src/tui/components/Spinner.jsx +89 -95
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/dist/index.mjs +1694 -10633
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +110 -3
- package/src/tui/engine/session-api-ext.mjs +24 -5
- package/src/tui/engine/session-api.mjs +165 -55
- package/src/tui/engine/session-flow.mjs +50 -5
- package/src/tui/engine/tool-card-results.mjs +10 -2
- package/src/tui/engine/tool-result-text.mjs +10 -0
- package/src/tui/engine/turn.mjs +11 -64
- package/src/tui/engine-local-session.mjs +1124 -0
- package/src/tui/engine.mjs +16 -1065
- package/src/tui/index.jsx +41 -4
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +83 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -8,6 +8,7 @@ import { shutdownStdioChild, killStdioChildTreeFast } from './child-tree.mjs';
|
|
|
8
8
|
import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../../../shared/service-discovery.mjs';
|
|
9
9
|
import { makeToolEnvelope, normalizeToolEnvelope } from '../session/tool-envelope.mjs';
|
|
10
10
|
import { classifyResultKind } from '../session/result-classification.mjs';
|
|
11
|
+
import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
|
|
11
12
|
// --- Types ---
|
|
12
13
|
/** Known auto-detect targets: port file path relative to tmpdir.
|
|
13
14
|
* Note: `mixdog` used to self-loopback via active-instance.json's
|
|
@@ -19,10 +20,11 @@ const AUTO_DETECT_PORTS = {
|
|
|
19
20
|
'mixdog-memory': { discovery: 'memory', dir: 'mixdog', file: 'active-instance.json', portField: 'memory_port', endpoint: '/mcp' },
|
|
20
21
|
};
|
|
21
22
|
const DEFAULT_MCP_CALL_TIMEOUT_MS = 120000;
|
|
22
|
-
// Per-server STARTUP handshake budget (connect + listTools)
|
|
23
|
+
// Per-server STARTUP handshake budget (connect + listTools): 10s.
|
|
23
24
|
const DEFAULT_MCP_STARTUP_TIMEOUT_MS = 10000;
|
|
24
25
|
// --- State ---
|
|
25
26
|
const servers = new Map();
|
|
27
|
+
const reconnects = createKeyedSingleflight();
|
|
26
28
|
let mcpSdkPromise = null;
|
|
27
29
|
// Memo for mcpToolHasField(name, field) — keyed by `${toolName}|${field}`.
|
|
28
30
|
// The lookup (regex parse + servers Map get + tools.find + schema property
|
|
@@ -173,6 +175,26 @@ export function getMcpServerInstructionsMap() {
|
|
|
173
175
|
}
|
|
174
176
|
return out;
|
|
175
177
|
}
|
|
178
|
+
|
|
179
|
+
async function reconnectMcpServer(serverName, failedServer) {
|
|
180
|
+
return reconnects.run(serverName, async () => {
|
|
181
|
+
const current = servers.get(serverName);
|
|
182
|
+
// A peer already completed the replacement while this failed call was
|
|
183
|
+
// unwinding. Reuse it immediately instead of closing a fresh transport.
|
|
184
|
+
if (current && current !== failedServer) return current;
|
|
185
|
+
if (current) {
|
|
186
|
+
await _closeServer(current);
|
|
187
|
+
if (servers.get(serverName) === current) servers.delete(serverName);
|
|
188
|
+
}
|
|
189
|
+
await connectServer(serverName, failedServer.cfg);
|
|
190
|
+
const replacement = servers.get(serverName);
|
|
191
|
+
if (!replacement) {
|
|
192
|
+
throw new Error(`reconnect succeeded but server "${serverName}" entry is missing from registry`);
|
|
193
|
+
}
|
|
194
|
+
return replacement;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
176
198
|
/**
|
|
177
199
|
* Execute an MCP tool call.
|
|
178
200
|
* Name format: `mcp__{serverName}__{toolName}`
|
|
@@ -195,21 +217,14 @@ export async function executeMcpTool(name, args) {
|
|
|
195
217
|
mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
|
|
196
218
|
throw firstErr;
|
|
197
219
|
}
|
|
198
|
-
mcpLog(`[mcp-client] Tool call failed, attempting reconnect...\n`);
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
await _closeServer(server);
|
|
202
|
-
} catch { /* ignore close error */ }
|
|
220
|
+
mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
|
|
221
|
+
let retryServer;
|
|
203
222
|
try {
|
|
204
|
-
await
|
|
223
|
+
retryServer = await reconnectMcpServer(serverName, server);
|
|
205
224
|
} catch (reconnectErr) {
|
|
206
225
|
const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
|
|
207
226
|
throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
|
|
208
227
|
}
|
|
209
|
-
const retryServer = servers.get(serverName);
|
|
210
|
-
if (!retryServer) {
|
|
211
|
-
throw new Error(`Tool call failed: ${firstMsg}; reconnect succeeded but server "${serverName}" entry is missing from registry`);
|
|
212
|
-
}
|
|
213
228
|
try {
|
|
214
229
|
result = await _callToolWithTimeout(retryServer, toolName, args);
|
|
215
230
|
} catch (retryErr) {
|
|
@@ -264,8 +279,8 @@ function isMcpToolCallTimeoutError(err) {
|
|
|
264
279
|
}
|
|
265
280
|
|
|
266
281
|
// MCP per-server STARTUP timeout: bounds the connect + listTools handshake so a
|
|
267
|
-
// slow or hung server can't stall boot or the first turn. Default 10s
|
|
268
|
-
//
|
|
282
|
+
// slow or hung server can't stall boot or the first turn. Default 10s.
|
|
283
|
+
// Per-server override: startupTimeoutMs / startupTimeoutSec. Global
|
|
269
284
|
// env: MIXDOG_MCP_STARTUP_TIMEOUT_MS. A value of 0/off/none/false disables it.
|
|
270
285
|
export function resolveMcpStartupTimeoutMs(cfg = {}, env = process.env) {
|
|
271
286
|
const rawMs = cfg?.startupTimeoutMs ?? cfg?.startup_timeout_ms;
|
|
@@ -632,7 +647,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
632
647
|
...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
|
|
633
648
|
}));
|
|
634
649
|
const toolNames = tools.map(t => t.name);
|
|
635
|
-
servers.set(name, { name, client, transport, tools, cfg, instructions });
|
|
650
|
+
servers.set(name, { name, client, transport, tools, cfg, instructions, generation: genAtStart });
|
|
636
651
|
_invalidateMcpToolFieldMemo();
|
|
637
652
|
mcpLog(`[mcp] connected: ${tools.length} tools — ${toolNames.join(', ')}\n`);
|
|
638
653
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createKeyedSingleflight() {
|
|
2
|
+
const pending = new Map();
|
|
3
|
+
return {
|
|
4
|
+
run(key, task) {
|
|
5
|
+
const lane = String(key || '');
|
|
6
|
+
const existing = pending.get(lane);
|
|
7
|
+
if (existing) return existing;
|
|
8
|
+
let tracked;
|
|
9
|
+
const promise = Promise.resolve().then(task);
|
|
10
|
+
tracked = promise.finally(() => {
|
|
11
|
+
if (pending.get(lane) === tracked) pending.delete(lane);
|
|
12
|
+
});
|
|
13
|
+
pending.set(lane, tracked);
|
|
14
|
+
return tracked;
|
|
15
|
+
},
|
|
16
|
+
has(key) {
|
|
17
|
+
return pending.has(String(key || ''));
|
|
18
|
+
},
|
|
19
|
+
get size() {
|
|
20
|
+
return pending.size;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// the adaptive halving/recovery below is inert.
|
|
4
|
+
// Normal provider traffic is not concurrency-gated. Independent sessions and
|
|
5
|
+
// accounts start immediately; a finite limit exists only when an operator
|
|
6
|
+
// explicitly configures one, or after that account reports a real 429.
|
|
8
7
|
export const PROVIDER_ACCOUNT_CONCURRENCY = Infinity;
|
|
9
8
|
export const PROVIDER_ACCOUNT_MAX_QUEUE = 1024;
|
|
10
9
|
// A cooldown longer than this is a quota-window block (subscription limit),
|
|
@@ -13,6 +12,21 @@ export const PROVIDER_ACCOUNT_MAX_QUEUE = 1024;
|
|
|
13
12
|
export const PROVIDER_COOLDOWN_FAIL_FAST_MS = 60_000;
|
|
14
13
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
15
14
|
const currentAdmission = new AsyncLocalStorage();
|
|
15
|
+
const PRIORITY_RANK = Object.freeze({
|
|
16
|
+
'user-blocking': 0,
|
|
17
|
+
'user-visible': 1,
|
|
18
|
+
'best-effort': 2,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function normalizePriority(value) {
|
|
22
|
+
const key = String(value || 'user-visible').toLowerCase();
|
|
23
|
+
return Object.hasOwn(PRIORITY_RANK, key) ? key : 'user-visible';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function currentProviderAdmissionOwner() {
|
|
27
|
+
const active = currentAdmission.getStore();
|
|
28
|
+
return active?.ownerKey ? String(active.ownerKey) : null;
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
function abortError(signal, fallback = 'provider request canceled') {
|
|
18
32
|
return signal?.reason instanceof Error
|
|
@@ -110,10 +124,10 @@ export class ProviderAdmissionScheduler {
|
|
|
110
124
|
setTimer = setTimeout,
|
|
111
125
|
clearTimer = clearTimeout,
|
|
112
126
|
} = {}) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
const parsedConcurrency = Math.floor(Number(concurrency));
|
|
128
|
+
this.concurrency = Number.isFinite(parsedConcurrency) && parsedConcurrency >= 1
|
|
129
|
+
? parsedConcurrency
|
|
130
|
+
: Infinity;
|
|
117
131
|
this.maxQueue = configuredQueueBound(maxQueue);
|
|
118
132
|
this.now = now;
|
|
119
133
|
this.setTimer = setTimer;
|
|
@@ -160,7 +174,9 @@ export class ProviderAdmissionScheduler {
|
|
|
160
174
|
limit: this.concurrency,
|
|
161
175
|
cooldownUntil: 0,
|
|
162
176
|
recoverySuccesses: 0,
|
|
177
|
+
recoveryTarget: null,
|
|
163
178
|
cooldownTimer: null,
|
|
179
|
+
fairCursor: null,
|
|
164
180
|
};
|
|
165
181
|
if (!lane.adaptive || until <= lane.cooldownUntil) return false;
|
|
166
182
|
this.lanes.set(laneKey, lane);
|
|
@@ -175,8 +191,15 @@ export class ProviderAdmissionScheduler {
|
|
|
175
191
|
return true;
|
|
176
192
|
}
|
|
177
193
|
|
|
178
|
-
run(key, task, {
|
|
194
|
+
run(key, task, {
|
|
195
|
+
signal = null,
|
|
196
|
+
onCooldownWait = null,
|
|
197
|
+
ownerKey = null,
|
|
198
|
+
priority = 'user-visible',
|
|
199
|
+
} = {}) {
|
|
179
200
|
const laneKey = String(key || 'provider:default');
|
|
201
|
+
const owner = String(ownerKey || 'anonymous').trim().slice(0, 240) || 'anonymous';
|
|
202
|
+
const taskPriority = normalizePriority(priority);
|
|
180
203
|
// Provider-local recovery may recursively call this.send(). It already
|
|
181
204
|
// owns a slot, so reacquiring the same lane could deadlock a full wave.
|
|
182
205
|
const current = this.context.getStore();
|
|
@@ -193,7 +216,9 @@ export class ProviderAdmissionScheduler {
|
|
|
193
216
|
limit: this.concurrency,
|
|
194
217
|
cooldownUntil: 0,
|
|
195
218
|
recoverySuccesses: 0,
|
|
219
|
+
recoveryTarget: null,
|
|
196
220
|
cooldownTimer: null,
|
|
221
|
+
fairCursor: null,
|
|
197
222
|
};
|
|
198
223
|
this.lanes.set(laneKey, lane);
|
|
199
224
|
// Long cooldown = quota window. Fail fast with a visible error; short
|
|
@@ -206,7 +231,17 @@ export class ProviderAdmissionScheduler {
|
|
|
206
231
|
return Promise.reject(new ProviderAdmissionQueueOverflowError(laneKey, this.maxQueue));
|
|
207
232
|
}
|
|
208
233
|
return new Promise((resolve, reject) => {
|
|
209
|
-
const item = {
|
|
234
|
+
const item = {
|
|
235
|
+
task,
|
|
236
|
+
signal,
|
|
237
|
+
resolve,
|
|
238
|
+
reject,
|
|
239
|
+
canceled: false,
|
|
240
|
+
onAbort: null,
|
|
241
|
+
ownerKey: owner,
|
|
242
|
+
priority: taskPriority,
|
|
243
|
+
queuedAt: this.now(),
|
|
244
|
+
};
|
|
210
245
|
if (signal) {
|
|
211
246
|
item.onAbort = () => {
|
|
212
247
|
if (item.started || item.canceled) return;
|
|
@@ -236,6 +271,28 @@ export class ProviderAdmissionScheduler {
|
|
|
236
271
|
});
|
|
237
272
|
}
|
|
238
273
|
|
|
274
|
+
_nextQueueIndex(lane) {
|
|
275
|
+
if (!lane.queue.length) return -1;
|
|
276
|
+
const bestRank = Math.min(...lane.queue.map((item) =>
|
|
277
|
+
PRIORITY_RANK[normalizePriority(item.priority)]));
|
|
278
|
+
const eligible = lane.queue
|
|
279
|
+
.map((item, index) => ({ item, index }))
|
|
280
|
+
.filter(({ item }) => PRIORITY_RANK[normalizePriority(item.priority)] === bestRank);
|
|
281
|
+
const owners = [];
|
|
282
|
+
for (const { item } of eligible) {
|
|
283
|
+
const owner = item.ownerKey || 'anonymous';
|
|
284
|
+
if (!owners.includes(owner)) owners.push(owner);
|
|
285
|
+
}
|
|
286
|
+
let selected = owners[0] || 'anonymous';
|
|
287
|
+
if (owners.length > 1) {
|
|
288
|
+
const prior = owners.indexOf(lane.fairCursor);
|
|
289
|
+
selected = owners[(prior + 1 + owners.length) % owners.length];
|
|
290
|
+
}
|
|
291
|
+
lane.fairCursor = selected;
|
|
292
|
+
return eligible.find(({ item }) => (item.ownerKey || 'anonymous') === selected)?.index
|
|
293
|
+
?? eligible[0].index;
|
|
294
|
+
}
|
|
295
|
+
|
|
239
296
|
_drain(key, lane) {
|
|
240
297
|
if (lane.adaptive && lane.cooldownUntil > this.now()) {
|
|
241
298
|
if (lane.cooldownUntil - this.now() > PROVIDER_COOLDOWN_FAIL_FAST_MS) {
|
|
@@ -245,7 +302,9 @@ export class ProviderAdmissionScheduler {
|
|
|
245
302
|
return;
|
|
246
303
|
}
|
|
247
304
|
while (lane.active < lane.limit && lane.queue.length) {
|
|
248
|
-
const
|
|
305
|
+
const index = this._nextQueueIndex(lane);
|
|
306
|
+
if (index < 0) break;
|
|
307
|
+
const [item] = lane.queue.splice(index, 1);
|
|
249
308
|
if (item.canceled) {
|
|
250
309
|
this._detach(item);
|
|
251
310
|
continue;
|
|
@@ -268,7 +327,14 @@ export class ProviderAdmissionScheduler {
|
|
|
268
327
|
item.signal.addEventListener('abort', running.parentAbort, { once: true });
|
|
269
328
|
}
|
|
270
329
|
this.running.add(running);
|
|
271
|
-
const admission = {
|
|
330
|
+
const admission = {
|
|
331
|
+
scheduler: this,
|
|
332
|
+
key,
|
|
333
|
+
lane,
|
|
334
|
+
ownerKey: item.ownerKey,
|
|
335
|
+
priority: item.priority,
|
|
336
|
+
rateLimited: false,
|
|
337
|
+
};
|
|
272
338
|
this.context.run(admission, () => Promise.resolve().then(() => item.task(controller.signal)))
|
|
273
339
|
.then((value) => {
|
|
274
340
|
// A retry that eventually succeeds is not evidence that
|
|
@@ -292,6 +358,25 @@ export class ProviderAdmissionScheduler {
|
|
|
292
358
|
if (this._canDeleteLane(lane)) this._deleteLane(key, lane);
|
|
293
359
|
}
|
|
294
360
|
|
|
361
|
+
snapshot() {
|
|
362
|
+
const now = this.now();
|
|
363
|
+
return {
|
|
364
|
+
concurrency: Number.isFinite(this.concurrency) ? this.concurrency : null,
|
|
365
|
+
lanes: [...this.lanes.entries()].map(([key, lane]) => ({
|
|
366
|
+
key,
|
|
367
|
+
active: lane.active,
|
|
368
|
+
queued: lane.queue.length,
|
|
369
|
+
owners: new Set(lane.queue.map((item) => item.ownerKey || 'anonymous')).size,
|
|
370
|
+
limit: Number.isFinite(lane.limit) ? lane.limit : null,
|
|
371
|
+
cooldownMs: Math.max(0, lane.cooldownUntil - now),
|
|
372
|
+
oldestWaitMs: lane.queue.reduce(
|
|
373
|
+
(oldest, item) => Math.max(oldest, Math.max(0, now - (item.queuedAt || now))),
|
|
374
|
+
0,
|
|
375
|
+
),
|
|
376
|
+
})),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
295
380
|
_recordFailure(key, lane, error) {
|
|
296
381
|
if (!lane.adaptive) return false;
|
|
297
382
|
const status = Number(error?.httpStatus || error?.status || error?.response?.status || 0);
|
|
@@ -300,7 +385,14 @@ export class ProviderAdmissionScheduler {
|
|
|
300
385
|
if (this.reportedRateLimits.has(error)) return false;
|
|
301
386
|
this.reportedRateLimits.add(error);
|
|
302
387
|
}
|
|
303
|
-
|
|
388
|
+
const observed = Number.isFinite(lane.limit)
|
|
389
|
+
? lane.limit
|
|
390
|
+
: Math.max(1, lane.active);
|
|
391
|
+
lane.recoveryTarget = Math.max(
|
|
392
|
+
Number(lane.recoveryTarget) || 0,
|
|
393
|
+
observed,
|
|
394
|
+
);
|
|
395
|
+
lane.limit = Math.max(1, Math.floor(observed / 2));
|
|
304
396
|
lane.recoverySuccesses = 0;
|
|
305
397
|
const now = this.now();
|
|
306
398
|
lane.cooldownUntil = Math.max(
|
|
@@ -348,6 +440,7 @@ export class ProviderAdmissionScheduler {
|
|
|
348
440
|
lane.cooldownUntil = 0;
|
|
349
441
|
lane.limit = this.concurrency;
|
|
350
442
|
lane.recoverySuccesses = 0;
|
|
443
|
+
lane.recoveryTarget = null;
|
|
351
444
|
if (lane.cooldownTimer) {
|
|
352
445
|
this.clearTimer(lane.cooldownTimer);
|
|
353
446
|
lane.cooldownTimer = null;
|
|
@@ -364,11 +457,15 @@ export class ProviderAdmissionScheduler {
|
|
|
364
457
|
}
|
|
365
458
|
|
|
366
459
|
_recordSuccess(lane) {
|
|
367
|
-
if (!lane.adaptive || lane.limit
|
|
460
|
+
if (!lane.adaptive || !Number.isFinite(lane.limit)) return;
|
|
368
461
|
lane.recoverySuccesses += 1;
|
|
369
462
|
if (lane.recoverySuccesses >= lane.limit) {
|
|
370
463
|
lane.limit += 1;
|
|
371
464
|
lane.recoverySuccesses = 0;
|
|
465
|
+
if (lane.recoveryTarget && lane.limit >= lane.recoveryTarget) {
|
|
466
|
+
lane.limit = this.concurrency;
|
|
467
|
+
lane.recoveryTarget = null;
|
|
468
|
+
}
|
|
372
469
|
}
|
|
373
470
|
}
|
|
374
471
|
|
|
@@ -505,6 +602,8 @@ export function wrapProviderAdmission(provider, providerName, scheduler = provid
|
|
|
505
602
|
});
|
|
506
603
|
}, {
|
|
507
604
|
signal,
|
|
605
|
+
ownerKey: opts.admissionOwner || opts.sessionId || null,
|
|
606
|
+
priority: opts.admissionPriority || 'user-visible',
|
|
508
607
|
onCooldownWait: (waitMs) => {
|
|
509
608
|
// Display-only: the TUI/desktop 'reconnecting' stage already
|
|
510
609
|
// renders a custom verb, so no new stage vocabulary is needed.
|
|
@@ -17,6 +17,67 @@ function _sortEffortLevels(levels) {
|
|
|
17
17
|
return [...levels].sort((a, b) => rank(a) - rank(b));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Control flags that ride the capabilities.effort map alongside real levels
|
|
21
|
+
// (`{supported:true, low:true, …}`). They are NOT selectable effort levels;
|
|
22
|
+
// letting them through minted a bogus "supported" level that got persisted
|
|
23
|
+
// into the on-disk catalog and offered in the UI.
|
|
24
|
+
const CONTROL_EFFORT_KEYS = new Set(['supported', 'enabled', 'default']);
|
|
25
|
+
|
|
26
|
+
// ── Catalog-first capability index ───────────────────────────────────────
|
|
27
|
+
// The provider catalog already carries what each model advertises
|
|
28
|
+
// (capabilities.effort → reasoningOptions:[{type:'effort',values:[…]}]), so it
|
|
29
|
+
// — not a hardcoded regex ladder — is the source of truth for what we put on
|
|
30
|
+
// the wire. anthropic-model-resolve feeds this whenever the in-memory catalog
|
|
31
|
+
// moves. The regex ladder below stays as the fallback for ids the catalog does
|
|
32
|
+
// not know (offline start, api-key provider, first run before /v1/models).
|
|
33
|
+
const _catalogEffortLevels = new Map();
|
|
34
|
+
|
|
35
|
+
function _catalogKeys(model) {
|
|
36
|
+
const id = normalizeModelId(model);
|
|
37
|
+
if (!id) return [];
|
|
38
|
+
// A dated id and its bare version alias describe the SAME model — index
|
|
39
|
+
// both so `claude-opus-5` and `claude-opus-5-20260101` resolve alike.
|
|
40
|
+
const undated = id.replace(/-\d{8}$/, '');
|
|
41
|
+
return undated !== id ? [id, undated] : [id];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Replace the catalog-derived capability index. Records without a
|
|
46
|
+
* `reasoningOptions` array are treated as UNKNOWN (skipped, so the regex
|
|
47
|
+
* fallback still applies) rather than as "no effort" — offline/static fallback
|
|
48
|
+
* lists carry no capability data and must not disable effort.
|
|
49
|
+
*/
|
|
50
|
+
export function setModelEffortCapabilities(models) {
|
|
51
|
+
_catalogEffortLevels.clear();
|
|
52
|
+
if (!Array.isArray(models)) return;
|
|
53
|
+
for (const model of models) {
|
|
54
|
+
if (!model?.id || !Array.isArray(model.reasoningOptions)) continue;
|
|
55
|
+
const option = model.reasoningOptions.find(
|
|
56
|
+
(entry) => String(entry?.type || '').trim().toLowerCase() === 'effort',
|
|
57
|
+
);
|
|
58
|
+
const levels = new Set(
|
|
59
|
+
(Array.isArray(option?.values) ? option.values : [])
|
|
60
|
+
.map((value) => String(value || '').trim().toLowerCase())
|
|
61
|
+
.filter((value) => value && !CONTROL_EFFORT_KEYS.has(value)),
|
|
62
|
+
);
|
|
63
|
+
for (const key of _catalogKeys(model.id)) {
|
|
64
|
+
const existing = _catalogEffortLevels.get(key);
|
|
65
|
+
// A bare alias shared by several records keeps the richer entry.
|
|
66
|
+
if (existing?.size && levels.size === 0) continue;
|
|
67
|
+
_catalogEffortLevels.set(key, levels);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Advertised levels for `model`, or null when the catalog has no record. */
|
|
73
|
+
function _catalogLevels(model) {
|
|
74
|
+
for (const key of _catalogKeys(model)) {
|
|
75
|
+
const levels = _catalogEffortLevels.get(key);
|
|
76
|
+
if (levels) return levels;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
20
81
|
export const EFFORT_BETA_HEADER = 'effort-2025-11-24';
|
|
21
82
|
|
|
22
83
|
export const LEGACY_EFFORT_BUDGET = Object.freeze({
|
|
@@ -40,7 +101,10 @@ function parseClaudeVersion(model) {
|
|
|
40
101
|
if (triple) {
|
|
41
102
|
return { family: triple[1], major: Number(triple[2]), minor: Number(triple[3]) };
|
|
42
103
|
}
|
|
43
|
-
|
|
104
|
+
// Bare version ids carry family + major only (claude-opus-5). opus/haiku
|
|
105
|
+
// were missing here, so `claude-opus-5` parsed to null and fell through
|
|
106
|
+
// every capability check as an unknown shape.
|
|
107
|
+
const pair = m.match(/^claude-(sonnet|fable|opus|haiku)-(\d+)(?:$|[-@])/);
|
|
44
108
|
if (pair) {
|
|
45
109
|
return { family: pair[1], major: Number(pair[2]), minor: null };
|
|
46
110
|
}
|
|
@@ -64,6 +128,9 @@ function isLegacyAnthropicReasoningModel(model) {
|
|
|
64
128
|
if (parsed.family === 'sonnet' || parsed.family === 'opus') {
|
|
65
129
|
if (parsed.major < 4) return true;
|
|
66
130
|
if (parsed.major === 4 && parsed.minor !== null && parsed.minor < 6) return true;
|
|
131
|
+
// Bare major, no minor (claude-opus-4 / claude-sonnet-4): adaptive
|
|
132
|
+
// effort only ships from 5 up, so 4.x aliases stay manual-thinking.
|
|
133
|
+
if (parsed.minor === null && parsed.major < 5) return true;
|
|
67
134
|
return false;
|
|
68
135
|
}
|
|
69
136
|
return false;
|
|
@@ -72,13 +139,22 @@ function isLegacyAnthropicReasoningModel(model) {
|
|
|
72
139
|
// @[MODEL LAUNCH]: extend allowlist when new Claude models ship with effort support.
|
|
73
140
|
export function modelSupportsEffort(model) {
|
|
74
141
|
if (isEnvTruthy(process.env.MIXDOG_ANTHROPIC_ALWAYS_ENABLE_EFFORT)) return true;
|
|
142
|
+
const advertised = _catalogLevels(model);
|
|
143
|
+
if (advertised) return advertised.size > 0;
|
|
144
|
+
return _regexSupportsEffort(model);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fallback ladder for ids the catalog does not carry. Also used by
|
|
148
|
+
// effortValuesForModel, which BUILDS the catalog records and therefore must
|
|
149
|
+
// never consult the index it feeds.
|
|
150
|
+
function _regexSupportsEffort(model) {
|
|
75
151
|
const m = normalizeModelId(model);
|
|
76
152
|
if (!m.includes('claude')) return false;
|
|
77
153
|
if (isLegacyAnthropicReasoningModel(model)) return false;
|
|
78
154
|
if (m.includes('opus-4-6') || m.includes('sonnet-4-6')) return true;
|
|
79
155
|
if (m.includes('sonnet-5') || m.includes('fable-5')) return true;
|
|
80
156
|
if (/^claude-opus-4-(6|7|8)(?:$|[-@])/.test(m)) return true;
|
|
81
|
-
if (/^claude-opus-5
|
|
157
|
+
if (/^claude-opus-5(?:$|[-@])/.test(m)) return true;
|
|
82
158
|
// Fallthrough for not-yet-enumerated modern models: only grant effort when
|
|
83
159
|
// parseClaudeVersion resolves a family with major>=4 (and not a manual-
|
|
84
160
|
// thinking legacy already excluded above). A bare `startsWith('claude-')`
|
|
@@ -98,6 +174,12 @@ export function modelSupportsEffort(model) {
|
|
|
98
174
|
// supported by: Fable 5, Mythos 5, Opus 4.8, Opus 4.7, Sonnet 5.
|
|
99
175
|
// NOT Opus 4.6 / Sonnet 4.6 (those support max but not xhigh).
|
|
100
176
|
export function modelSupportsXhighEffort(model) {
|
|
177
|
+
const advertised = _catalogLevels(model);
|
|
178
|
+
if (advertised) return advertised.has('xhigh');
|
|
179
|
+
return _regexSupportsXhighEffort(model);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _regexSupportsXhighEffort(model) {
|
|
101
183
|
const m = normalizeModelId(model);
|
|
102
184
|
if (/^claude-opus-4-(7|8)(?:$|[-@])/.test(m)) return true;
|
|
103
185
|
if (/^claude-opus-5(?:$|[-@])/.test(m)) return true;
|
|
@@ -111,7 +193,14 @@ export function modelSupportsXhighEffort(model) {
|
|
|
111
193
|
// @[MODEL LAUNCH]: extend when new Opus models support max effort.
|
|
112
194
|
// Max list = xhigh list PLUS Opus 4.6, Sonnet 4.6, Opus 4.5.
|
|
113
195
|
export function modelSupportsMaxEffort(model) {
|
|
114
|
-
|
|
196
|
+
const advertised = _catalogLevels(model);
|
|
197
|
+
// xhigh support implies max support (kept from the ladder below).
|
|
198
|
+
if (advertised) return advertised.has('max') || advertised.has('xhigh');
|
|
199
|
+
return _regexSupportsMaxEffort(model);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _regexSupportsMaxEffort(model) {
|
|
203
|
+
if (_regexSupportsXhighEffort(model)) return true;
|
|
115
204
|
const m = normalizeModelId(model);
|
|
116
205
|
if (/^claude-opus-4-(5|6)(?:$|[-@])/.test(m)) return true;
|
|
117
206
|
if (/^claude-sonnet-4-6(?:$|[-@])/.test(m)) return true;
|
|
@@ -209,7 +298,7 @@ export function applyAnthropicEffortToBody(
|
|
|
209
298
|
// Set unconditionally (independent of `normalized`) so effort-capable
|
|
210
299
|
// turns always carry adaptive thinking + round-trip signatures.
|
|
211
300
|
// MIXDOG_ANTHROPIC_THINKING_DISPLAY=omitted (operator/bench knob):
|
|
212
|
-
//
|
|
301
|
+
// thinking blocks are omitted entirely, so nothing is
|
|
213
302
|
// replayed into later requests (saves the 1h cache-write + re-read on
|
|
214
303
|
// accumulated thinking) at the cost of losing visible reasoning and
|
|
215
304
|
// cross-iteration thinking continuity. Default stays summarized.
|
|
@@ -254,7 +343,8 @@ export function effortValuesForModel(capabilities, modelId) {
|
|
|
254
343
|
// advertises (xhigh, max, or anything future), no hardcoded allowlist.
|
|
255
344
|
if (effort !== true && typeof effort === 'object') {
|
|
256
345
|
const advertised = Object.keys(effort).filter(
|
|
257
|
-
(level) =>
|
|
346
|
+
(level) => !CONTROL_EFFORT_KEYS.has(String(level).trim().toLowerCase())
|
|
347
|
+
&& (effort[level] === true || effort[level]?.supported === true),
|
|
258
348
|
);
|
|
259
349
|
if (advertised.length) return _sortEffortLevels(advertised);
|
|
260
350
|
// Object with no per-level flags: fall through to the boolean/supported
|
|
@@ -265,10 +355,10 @@ export function effortValuesForModel(capabilities, modelId) {
|
|
|
265
355
|
// the model supports effort but doesn't enumerate levels, so derive the
|
|
266
356
|
// set from the known level ladder, gated by the top-tier model check.
|
|
267
357
|
let levels = [...EFFORT_LEVELS];
|
|
268
|
-
if (!
|
|
358
|
+
if (!_regexSupportsMaxEffort(modelId)) {
|
|
269
359
|
levels = levels.filter((level) => level !== 'max');
|
|
270
360
|
}
|
|
271
|
-
if (!
|
|
361
|
+
if (!_regexSupportsXhighEffort(modelId)) {
|
|
272
362
|
levels = levels.filter((level) => level !== 'xhigh');
|
|
273
363
|
}
|
|
274
364
|
return levels;
|
|
@@ -8,20 +8,22 @@
|
|
|
8
8
|
import { enrichModels } from './model-catalog.mjs';
|
|
9
9
|
import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
10
10
|
import { makeModelCache } from './model-cache.mjs';
|
|
11
|
-
import { effortValuesForModel } from './anthropic-effort.mjs';
|
|
11
|
+
import { effortValuesForModel, setModelEffortCapabilities } from './anthropic-effort.mjs';
|
|
12
12
|
|
|
13
13
|
// Disk-backed cache so repeated process starts (cron, tool calls) don't
|
|
14
14
|
// hammer /v1/models. 24h TTL matches the upstream client cadence.
|
|
15
15
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
16
16
|
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
17
17
|
// discarded instead of misread.
|
|
18
|
-
|
|
18
|
+
// v2: effort level lists no longer carry the `supported` control flag as a
|
|
19
|
+
// selectable level, so v1 caches are discarded rather than replayed.
|
|
20
|
+
const ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION = 2;
|
|
19
21
|
|
|
20
22
|
const _modelCache = makeModelCache({
|
|
21
23
|
fileName: 'anthropic-oauth-models.json',
|
|
22
24
|
ttlMs: MODEL_CACHE_TTL_MS,
|
|
23
25
|
version: ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION,
|
|
24
|
-
onSave: (m) => {
|
|
26
|
+
onSave: (m) => { _setInMemoryCatalog(m); },
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
// Async wrappers so callers can keep awaiting; the shared cache CRUD is sync.
|
|
@@ -42,6 +44,9 @@ let _inMemoryCatalog = null;
|
|
|
42
44
|
// listModels() warm path, so expose a setter instead of the raw binding.
|
|
43
45
|
export function _setInMemoryCatalog(models) {
|
|
44
46
|
_inMemoryCatalog = Array.isArray(models) ? models.slice() : null;
|
|
47
|
+
// The request builder resolves effort capability from the catalog, so the
|
|
48
|
+
// capability index moves with the mirror — one write, one source of truth.
|
|
49
|
+
setModelEffortCapabilities(_inMemoryCatalog || []);
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
export function _catalogHas(id) {
|
|
@@ -185,7 +190,9 @@ export function _catalogOutputTokens(model) {
|
|
|
185
190
|
try {
|
|
186
191
|
if (!Array.isArray(_inMemoryCatalog)) {
|
|
187
192
|
const cached = _modelCache.loadSync();
|
|
188
|
-
|
|
193
|
+
// Route the lazy warm through the setter: a direct assignment left
|
|
194
|
+
// the effort-capability index empty even though the mirror was warm.
|
|
195
|
+
if (Array.isArray(cached)) _setInMemoryCatalog(cached);
|
|
189
196
|
}
|
|
190
197
|
if (!Array.isArray(_inMemoryCatalog)) return null;
|
|
191
198
|
const entry = _inMemoryCatalog.find(m => m?.id === model);
|
|
@@ -1094,7 +1094,7 @@ export class AnthropicOAuthProvider {
|
|
|
1094
1094
|
continue;
|
|
1095
1095
|
}
|
|
1096
1096
|
const classifier = _classifyMidstreamError(err, midState);
|
|
1097
|
-
//
|
|
1097
|
+
// Stall recovery (2026-08-03 v3 postmortem): a
|
|
1098
1098
|
// stalled stream that exposed NOTHING (no text/thinking
|
|
1099
1099
|
// relayed, no tool emitted) is re-issued NON-STREAMING instead
|
|
1100
1100
|
// of retrying the same streaming shape. Effort-mode models can
|
|
@@ -1103,8 +1103,7 @@ export class AnthropicOAuthProvider {
|
|
|
1103
1103
|
// generation into the same timer (observed live: deterministic
|
|
1104
1104
|
// 4×~138s beheading, ~552s per turn), while the non-streaming
|
|
1105
1105
|
// transport simply waits for the full body (bounded by
|
|
1106
|
-
// PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS).
|
|
1107
|
-
// exactly this on its watchdog aborts. Replay is trivially
|
|
1106
|
+
// PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS). Replay is trivially
|
|
1108
1107
|
// safe here — nothing was relayed or dispatched.
|
|
1109
1108
|
if (classifier === 'stream_stalled'
|
|
1110
1109
|
&& _outcome?.replayUnsafe !== true
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
NATIVE_SERVER_TOOL_CALL_BLOCK_TYPES,
|
|
24
24
|
NATIVE_SERVER_TOOL_RESULT_BLOCK_TYPES,
|
|
25
25
|
} from './lib/anthropic-native-blocks.mjs';
|
|
26
|
+
import { parseProviderJsonBatch } from './stream-json-pool.mjs';
|
|
26
27
|
import { stampStreamOutcome, STREAM_TRANSPORTS, STREAM_OUTCOME_VERSION } from './lib/stream-outcome.mjs';
|
|
27
28
|
|
|
28
29
|
/** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs.
|
|
@@ -486,7 +487,10 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
486
487
|
if (!data) continue;
|
|
487
488
|
|
|
488
489
|
try {
|
|
489
|
-
|
|
490
|
+
// A completed reader.read() owns this frame even if abort
|
|
491
|
+
// races immediately afterward; the next read observes the
|
|
492
|
+
// cancellation after this frame is relayed.
|
|
493
|
+
const [event] = await parseProviderJsonBatch([data]);
|
|
490
494
|
|
|
491
495
|
// SEMANTIC idle reset (Part A): reset the idle timer ONLY for
|
|
492
496
|
// real progress events, NOT for Anthropic keepalives. Anthropic
|
|
@@ -604,7 +604,7 @@ export class AnthropicProvider {
|
|
|
604
604
|
continue;
|
|
605
605
|
}
|
|
606
606
|
const classifier = _classifyMidstreamError(err, midState);
|
|
607
|
-
//
|
|
607
|
+
// Stall recovery (shared with anthropic-oauth,
|
|
608
608
|
// 2026-08-03): a stalled stream that exposed NOTHING is
|
|
609
609
|
// re-issued non-streaming instead of retrying the same
|
|
610
610
|
// streaming shape into the same idle window. Replay is
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
* codex-client-meta.mjs — codex client identity headers for the OpenAI OAuth
|
|
3
3
|
* transports.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* WS handshake:
|
|
5
|
+
* The reference client sends two client-identity headers on EVERY request,
|
|
6
|
+
* including the WS handshake:
|
|
7
7
|
* - `User-Agent: codex_cli_rs/<version> (<os> <ver>; <arch>) <terminal>`
|
|
8
|
-
* (login/src/auth/default_client.rs get_codex_user_agent + default_headers)
|
|
9
8
|
* - `version: <CARGO_PKG_VERSION>` via the built-in provider http_headers
|
|
10
|
-
*
|
|
11
|
-
* merge_request_headers (codex-api/src/endpoint/responses_websocket.rs).
|
|
9
|
+
* merged into the WS handshake.
|
|
12
10
|
* The backend uses these for client gating (model catalog visibility measured
|
|
13
11
|
* 2026-07-03) and plausibly for x-codex-turn-state issuance / sticky
|
|
14
12
|
* cache-node routing, so mixdog mirrors both.
|
|
@@ -17,7 +15,7 @@ import os from 'node:os';
|
|
|
17
15
|
|
|
18
16
|
// Offline fallback only; live value refreshes from npm (24h TTL, in-process).
|
|
19
17
|
// The backend gates model exposure AND per-request model access on the client
|
|
20
|
-
// version (gpt-5.6-* require >= 0.144.0 per
|
|
18
|
+
// version (gpt-5.6-* require >= 0.144.0 per the published model catalog,
|
|
21
19
|
// verified 2026-07-09), so keep this at the current release when bumping.
|
|
22
20
|
const CODEX_CLIENT_VERSION_FLOOR = '0.144.1';
|
|
23
21
|
const VERSION_TTL_MS = 24 * 60 * 60_000;
|