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.
Files changed (259) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +109 -0
  5. package/package.json +26 -13
  6. package/scripts/.tmp-coverage-report.mjs +96 -0
  7. package/scripts/build-tui.mjs +17 -1
  8. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  9. package/scripts/lib/isolated-root-cleanup.mjs +19 -0
  10. package/scripts/run-suite.mjs +100 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/lib/rules-builder.cjs +4 -3
  13. package/src/output-styles/detailed.md +13 -15
  14. package/src/output-styles/extreme-minimal.md +7 -11
  15. package/src/output-styles/minimal.md +4 -7
  16. package/src/output-styles/simple.md +11 -13
  17. package/src/rules/agent/30-explorer.md +7 -4
  18. package/src/rules/lead/01-general.md +1 -2
  19. package/src/rules/lead/lead-brief.md +4 -5
  20. package/src/rules/lead/lead-tool.md +3 -2
  21. package/src/rules/shared/01-tool.md +20 -35
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  24. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  26. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +13 -2
  27. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
  28. package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
  29. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  30. package/src/runtime/agent/orchestrator/mcp/client.mjs +29 -14
  31. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  33. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
  34. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
  39. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  40. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  41. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  42. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
  44. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  46. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +29 -16
  47. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  49. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
  50. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
  52. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +13 -14
  53. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
  54. package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
  55. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +82 -22
  56. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  57. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  58. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +40 -36
  59. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
  60. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
  61. package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
  62. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -33
  63. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
  64. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
  65. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  66. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
  67. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  68. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  69. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
  70. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
  71. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -4
  72. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +168 -83
  75. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
  79. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +34 -18
  80. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  81. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  82. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +89 -11
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
  84. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +6 -39
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -6
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -4
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +56 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +124 -19
  90. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  91. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  92. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +7 -16
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
  100. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
  102. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
  103. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +24 -10
  104. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +20 -43
  106. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +76 -3
  107. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +13 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -5
  109. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +38 -13
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
  113. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +8 -6
  114. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +138 -25
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
  117. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
  118. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
  119. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
  120. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  121. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -2
  123. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
  124. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +177 -16
  125. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +21 -1
  126. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +217 -25
  127. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
  128. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +14 -15
  129. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +60 -8
  130. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  131. package/src/runtime/channels/backends/discord.mjs +5 -14
  132. package/src/runtime/channels/backends/telegram.mjs +0 -5
  133. package/src/runtime/channels/lib/config.mjs +2 -2
  134. package/src/runtime/channels/lib/inbound-handler.mjs +1 -2
  135. package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
  136. package/src/runtime/channels/lib/scheduler.mjs +3 -3
  137. package/src/runtime/channels/lib/worker-main.mjs +1 -1
  138. package/src/runtime/media/renditions.mjs +21 -2
  139. package/src/runtime/memory/index.mjs +5 -38
  140. package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
  141. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  142. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  143. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
  144. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  145. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  146. package/src/runtime/memory/lib/http-router.mjs +8 -0
  147. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  148. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  149. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  150. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  151. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  152. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  153. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  154. package/src/runtime/memory/tool-defs.mjs +7 -11
  155. package/src/runtime/search/tool-defs.mjs +2 -2
  156. package/src/runtime/shared/agent-route-config.mjs +119 -0
  157. package/src/runtime/shared/atomic-file.mjs +133 -4
  158. package/src/runtime/shared/background-tasks.mjs +6 -3
  159. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  160. package/src/runtime/shared/child-guardian.mjs +204 -108
  161. package/src/runtime/shared/child-spawn-gate.mjs +60 -31
  162. package/src/runtime/shared/config.mjs +130 -44
  163. package/src/runtime/shared/resource-admission.mjs +146 -58
  164. package/src/runtime/shared/stream-progress.mjs +6 -0
  165. package/src/runtime/shared/tool-status.mjs +10 -1
  166. package/src/runtime/shared/tool-surface.mjs +2 -7
  167. package/src/runtime/shared/turn-snapshot.mjs +411 -21
  168. package/src/runtime/shared/turn-worktree-snapshot.mjs +552 -0
  169. package/src/session-runtime/config-helpers.mjs +39 -37
  170. package/src/session-runtime/context-status.mjs +9 -3
  171. package/src/session-runtime/lifecycle-api.mjs +35 -5
  172. package/src/session-runtime/mcp-glue.mjs +11 -6
  173. package/src/session-runtime/model-route-api.mjs +7 -5
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +94 -43
  176. package/src/session-runtime/runtime-core.mjs +57 -19
  177. package/src/session-runtime/runtime-tunables.mjs +4 -0
  178. package/src/session-runtime/self-update.mjs +33 -4
  179. package/src/session-runtime/session-lifecycle.mjs +11 -2
  180. package/src/session-runtime/session-text.mjs +2 -1
  181. package/src/session-runtime/session-turn-api.mjs +139 -23
  182. package/src/session-runtime/settings-api.mjs +5 -17
  183. package/src/session-runtime/tool-defs.mjs +3 -3
  184. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  185. package/src/session-runtime/workflow.mjs +25 -28
  186. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  187. package/src/standalone/agent-tool/helpers.mjs +2 -2
  188. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  189. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  190. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  191. package/src/standalone/agent-tool.mjs +4 -4
  192. package/src/standalone/backend-daemon.mjs +631 -0
  193. package/src/standalone/channel-admin.mjs +12 -12
  194. package/src/standalone/channel-daemon-client.mjs +17 -1
  195. package/src/standalone/channel-daemon-transport.mjs +231 -9
  196. package/src/standalone/channel-worker.mjs +3 -2
  197. package/src/standalone/engine-daemon-client.mjs +1069 -0
  198. package/src/standalone/engine-daemon-protocol.mjs +32 -0
  199. package/src/standalone/engine-daemon-service.mjs +1108 -0
  200. package/src/standalone/engine-daemon-transport.mjs +794 -0
  201. package/src/standalone/explore-tool.mjs +187 -70
  202. package/src/standalone/fair-call-scheduler.mjs +264 -0
  203. package/src/standalone/session-protocol.mjs +188 -0
  204. package/src/tui/App.jsx +92 -64
  205. package/src/tui/app/app-format.mjs +4 -2
  206. package/src/tui/app/app-view.jsx +10 -2
  207. package/src/tui/app/channel-pickers.mjs +7 -6
  208. package/src/tui/app/core-memory-picker.mjs +20 -20
  209. package/src/tui/app/doctor.mjs +5 -11
  210. package/src/tui/app/extension-pickers.mjs +20 -18
  211. package/src/tui/app/maintenance-pickers.mjs +27 -27
  212. package/src/tui/app/message-selector.mjs +103 -0
  213. package/src/tui/app/onboarding-steps.mjs +24 -20
  214. package/src/tui/app/project-picker.mjs +80 -56
  215. package/src/tui/app/prompt-submit.mjs +59 -43
  216. package/src/tui/app/resume-picker.mjs +2 -2
  217. package/src/tui/app/route-pickers.mjs +16 -9
  218. package/src/tui/app/settings-picker.mjs +60 -65
  219. package/src/tui/app/slash-dispatch.mjs +32 -28
  220. package/src/tui/app/transcript-window.mjs +57 -0
  221. package/src/tui/app/usage-context-panels.mjs +22 -7
  222. package/src/tui/app/use-mouse-input.mjs +25 -3
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +39 -17
  227. package/src/tui/app/use-transcript-scroll.mjs +39 -10
  228. package/src/tui/app/use-transcript-window.mjs +62 -14
  229. package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
  230. package/src/tui/components/PromptInput.jsx +81 -38
  231. package/src/tui/components/Spinner.jsx +89 -95
  232. package/src/tui/components/TextEntryPanel.jsx +14 -0
  233. package/src/tui/components/ToolExecution.jsx +2 -2
  234. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  235. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  236. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  237. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  238. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  239. package/src/tui/dist/index.mjs +1694 -10633
  240. package/src/tui/engine/agent-job-feed.mjs +2 -2
  241. package/src/tui/engine/live-share.mjs +110 -3
  242. package/src/tui/engine/session-api-ext.mjs +24 -5
  243. package/src/tui/engine/session-api.mjs +165 -55
  244. package/src/tui/engine/session-flow.mjs +50 -5
  245. package/src/tui/engine/tool-card-results.mjs +10 -2
  246. package/src/tui/engine/tool-result-text.mjs +10 -0
  247. package/src/tui/engine/turn.mjs +11 -64
  248. package/src/tui/engine-local-session.mjs +1124 -0
  249. package/src/tui/engine.mjs +16 -1065
  250. package/src/tui/index.jsx +41 -4
  251. package/src/tui/markdown/stream-fence.mjs +1 -1
  252. package/src/tui/spinner-meta.mjs +80 -0
  253. package/src/tui/spinner-verbs.mjs +83 -0
  254. package/src/ui/statusline-segments.mjs +43 -10
  255. package/src/ui/statusline.mjs +10 -1
  256. package/scripts/tmp-cdp-errors.mjs +0 -41
  257. package/scripts/tmp-cdp-inspect.mjs +0 -41
  258. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
  259. 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). Codex parity: 10s.
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
- await new Promise(r => setTimeout(r, 500));
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 connectServer(serverName, server.cfg);
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 (codex
268
- // parity). Per-server override: startupTimeoutMs / startupTimeoutSec. Global
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
- // Unbounded by design: provider requests must never serialize behind a local
5
- // concurrency cap. Rate-limit cooldowns below (server-mandated retry-after)
6
- // remain the only thing that parks/rejects requests; with an infinite limit
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
- this.concurrency = Math.min(
114
- PROVIDER_ACCOUNT_CONCURRENCY,
115
- Math.max(1, Math.floor(Number(concurrency) || PROVIDER_ACCOUNT_CONCURRENCY)),
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, { signal = null, onCooldownWait = null } = {}) {
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 = { task, signal, resolve, reject, canceled: false, onAbort: null };
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 item = lane.queue.shift();
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 = { scheduler: this, key, lane, rateLimited: false };
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
- lane.limit = Math.max(1, Math.floor(lane.limit / 2));
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 >= this.concurrency) return;
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
- const pair = m.match(/^claude-(sonnet|fable)-(\d+)(?:$|[-@])/);
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-/.test(m)) return true;
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
- if (modelSupportsXhighEffort(model)) return true;
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
- // CC-parity mode — no thinking blocks come back, so nothing is
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) => effort[level] === true || effort[level]?.supported === true,
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 (!modelSupportsMaxEffort(modelId)) {
358
+ if (!_regexSupportsMaxEffort(modelId)) {
269
359
  levels = levels.filter((level) => level !== 'max');
270
360
  }
271
- if (!modelSupportsXhighEffort(modelId)) {
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
- const ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION = 1;
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) => { _inMemoryCatalog = Array.isArray(m) ? m.slice() : null; },
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
- if (Array.isArray(cached)) _inMemoryCatalog = cached.slice();
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
- // CC-parity stall recovery (2026-08-03 v3 postmortem): a
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). Claude Code does
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
- const event = JSON.parse(data);
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
- // CC-parity stall recovery (ported from anthropic-oauth,
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
- * codex-rs sends two client-identity headers on EVERY request including the
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
- * (model-provider-info/src/lib.rs:339-343), merged into the WS handshake by
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 codex models-manager/models.json,
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;