mixdog 0.9.95 → 0.9.97

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 (282) 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 +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  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 +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -1,5 +1,9 @@
1
1
  import { resolvePluginData } from '../../shared/plugin-paths.mjs';
2
2
  import { readSection, updateSection, updateSectionAsync, getAgentApiKey, AGENT_PROVIDER_ENV } from '../../shared/config.mjs';
3
+ import {
4
+ agentRouteStorageNeedsMigration,
5
+ canonicalizeAgentRouteStorage,
6
+ } from '../../shared/agent-route-config.mjs';
3
7
  import { OPENAI_COMPAT_PRESETS } from './providers/openai-compat-presets.mjs';
4
8
  import {
5
9
  hasAnthropicOAuthCredentials,
@@ -7,20 +11,6 @@ import {
7
11
  hasGrokOAuthCredentials,
8
12
  } from './providers/oauth-credential-probes.mjs';
9
13
 
10
- // Keep config loading free of standalone provider-admin and its provider
11
- // runtimes. These identifiers mirror that module's static catalog, assembled
12
- // solely from the lightweight config/provider constants already imported here.
13
- const CONFIG_PROVIDER_IDS = new Set([
14
- ...Object.keys(AGENT_PROVIDER_ENV),
15
- ...Object.keys(OPENAI_COMPAT_PRESETS),
16
- 'openai-oauth',
17
- 'anthropic-oauth',
18
- 'grok-oauth',
19
- ]);
20
- function isConfiguredProviderId(provider) {
21
- return CONFIG_PROVIDER_IDS.has(String(provider || '').trim());
22
- }
23
-
24
14
  // Thin wrapper around resolvePluginData so callers in this orchestrator tree
25
15
  // can import a single helper without reaching into shared/.
26
16
  export function getPluginData() {
@@ -33,19 +23,13 @@ export function getPluginData() {
33
23
  // llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
34
24
  //
35
25
  // Explore and Maintainer start without a route so they dynamically inherit the
36
- // Main route. Explicit `maintenance.explore` / `maintenance.memory` choices
37
- // remain supported. The memory cycles (chunker / re-scorer / core reviewer)
38
- // share the optional `memory` preset knob — the cycle agents stay separate
39
- // (cycle1/2/3-agent, distinct slots and invokedBy) but resolve their model from
40
- // `maint.memory` via the `maintKey` override on their hidden-role entries.
26
+ // Main route. Their explicit routes live canonically in `agents.explore` and
27
+ // `agents.maintainer`; load-time migration still accepts the older workflow /
28
+ // maintenance aliases.
41
29
  // scheduler/webhook still let a per-entry config.json model win first (the
42
30
  // caller passes it explicitly via opts.preset); the haiku default below only
43
31
  // applies when an entry omits its own model.
44
- // Slots surfaced as tunable rows in the Maintenance Setup panel. This is the
45
- // UI / allow-list view (GET cleanup + POST validation) and is intentionally a
46
- // SUBSET of DEFAULT_MAINTENANCE: scheduler/webhook carry a per-entry model and
47
- // are not shown as shared rows, but still inherit the haiku default above when
48
- // an entry omits its own model.
32
+ // Legacy route slots accepted only at config ingress for migration.
49
33
  const MAINTENANCE_SLOTS = Object.freeze(['explore', 'memory']);
50
34
 
51
35
  // --- User profile (statusline /profile) -------------------------------------
@@ -213,6 +197,13 @@ function normalizeSearchRoute(route) {
213
197
  return null;
214
198
  const provider = normalizeAgentProviderId(route.provider);
215
199
  const model = String(route.model || '').trim();
200
+ if (!provider && model) {
201
+ return {
202
+ provider: 'default',
203
+ model: 'default',
204
+ ...(String(route.toolType || '').trim() ? { toolType: String(route.toolType).trim() } : {}),
205
+ };
206
+ }
216
207
  if (!provider || !model)
217
208
  return null;
218
209
  const out = { provider, model };
@@ -230,17 +221,11 @@ function normalizeSearchRoute(route) {
230
221
  // Normalize stored maintenance slots to the direct {provider, model} route
231
222
  // shape (provider/model/effort/fast only). Non-route values are dropped so the
232
223
  // DEFAULT_MAINTENANCE route fills the slot.
233
- function normalizeMaintenanceRoutes(rawMaint, defaultProvider) {
224
+ function normalizeMaintenanceRoutes(rawMaint) {
234
225
  const out = {};
235
- const configuredProvider = String(defaultProvider || '').trim();
236
- const fallbackProvider = isConfiguredProviderId(configuredProvider)
237
- ? configuredProvider
238
- : 'anthropic-oauth';
239
226
  for (const [slot, value] of Object.entries(rawMaint || {})) {
240
227
  if (value && typeof value === 'object' && !Array.isArray(value)) {
241
- const provider = normalizeAgentProviderId(
242
- value.provider || ((slot === 'explore' || slot === 'memory') ? fallbackProvider : ''),
243
- );
228
+ const provider = normalizeAgentProviderId(value.provider);
244
229
  const model = String(value.model || '').trim();
245
230
  if (provider && model) {
246
231
  const route = { provider, model };
@@ -281,6 +266,174 @@ function normalizeRecapConfig(rawRecap) {
281
266
  return recap;
282
267
  }
283
268
 
269
+ function configObject(value) {
270
+ return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};
271
+ }
272
+
273
+ function nonEmptyConfigObject(value) {
274
+ return Object.keys(value).length > 0 ? value : undefined;
275
+ }
276
+
277
+ function normalizeStoredCompactionType(value) {
278
+ const raw = String(value ?? '').trim().toLowerCase().replace(/_/g, '-');
279
+ if (['1', 'type1', 'type-1', 'semantic', 'summary', 'default'].includes(raw)) return 'semantic';
280
+ if (['2', 'type2', 'type-2', 'recall', 'recall-fast', 'recall-fasttrack',
281
+ 'recall-fast-track', 'fasttrack', 'fast-track'].includes(raw)) return 'recall-fasttrack';
282
+ return 'recall-fasttrack';
283
+ }
284
+
285
+ function canonicalizeAutoClearStorage(value) {
286
+ const raw = configObject(value);
287
+ const next = { ...raw };
288
+ const idleMs = Number(raw.idleMs ?? raw.thresholdMs ?? raw.idleMillis);
289
+ const providerSource = configObject(raw.providerIdleMs ?? raw.providerDefaults ?? raw.providers);
290
+ const providerIdleMs = {};
291
+ for (const [key, candidate] of Object.entries(providerSource)) {
292
+ const provider = String(key || '').trim().toLowerCase();
293
+ const duration = Number(candidate);
294
+ if (!provider || !Number.isFinite(duration) || duration <= 0) continue;
295
+ providerIdleMs[provider] = Math.max(60_000, Math.round(duration));
296
+ }
297
+ delete next.thresholdMs;
298
+ delete next.idleMillis;
299
+ delete next.providerDefaults;
300
+ delete next.providers;
301
+ delete next.custom;
302
+ if (Number.isFinite(idleMs) && idleMs > 0) next.idleMs = Math.max(60_000, Math.round(idleMs));
303
+ else delete next.idleMs;
304
+ if (Object.keys(providerIdleMs).length) next.providerIdleMs = providerIdleMs;
305
+ else delete next.providerIdleMs;
306
+ if (Object.prototype.hasOwnProperty.call(raw, 'enabled')) next.enabled = raw.enabled !== false;
307
+ if (Object.prototype.hasOwnProperty.call(raw, 'minContextPercent')) {
308
+ const percent = Number(raw.minContextPercent);
309
+ if (Number.isFinite(percent)) next.minContextPercent = Math.min(100, Math.max(0, Math.round(percent)));
310
+ else delete next.minContextPercent;
311
+ }
312
+ return nonEmptyConfigObject(next);
313
+ }
314
+
315
+ function canonicalizeCompactionStorage(value) {
316
+ const raw = configObject(value);
317
+ const next = { ...raw };
318
+ const hasType = ['type', 'compactType', 'compact_type']
319
+ .some((key) => Object.prototype.hasOwnProperty.call(raw, key));
320
+ if (hasType) next.type = normalizeStoredCompactionType(raw.type ?? raw.compactType ?? raw.compact_type);
321
+ if (Object.prototype.hasOwnProperty.call(raw, 'auto')
322
+ || Object.prototype.hasOwnProperty.call(raw, 'enabled')) {
323
+ next.auto = raw.auto !== false && raw.enabled !== false;
324
+ }
325
+ delete next.compactType;
326
+ delete next.compact_type;
327
+ delete next.enabled;
328
+ return nonEmptyConfigObject(next);
329
+ }
330
+
331
+ function canonicalizeShellStorage(value) {
332
+ const raw = configObject(value);
333
+ const next = { ...raw };
334
+ const command = String(raw.command ?? raw.path ?? raw.executable ?? raw.shell ?? '').trim();
335
+ delete next.path;
336
+ delete next.executable;
337
+ delete next.shell;
338
+ if (command) next.command = command;
339
+ else delete next.command;
340
+ return nonEmptyConfigObject(next);
341
+ }
342
+
343
+ function canonicalizeModulesStorage(value) {
344
+ const modules = configObject(value);
345
+ delete modules.memory;
346
+ return nonEmptyConfigObject(modules);
347
+ }
348
+
349
+ function migrateLegacyModelSettings(raw = {}) {
350
+ const modelSettings = raw.modelSettings && typeof raw.modelSettings === 'object'
351
+ ? { ...raw.modelSettings }
352
+ : {};
353
+ const legacyFastModels = raw.fastModels && typeof raw.fastModels === 'object'
354
+ ? raw.fastModels
355
+ : {};
356
+ for (const [key, enabled] of Object.entries(legacyFastModels)) {
357
+ if (enabled !== true) continue;
358
+ const current = modelSettings[key];
359
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
360
+ modelSettings[key] = { fast: true };
361
+ } else if (!Object.prototype.hasOwnProperty.call(current, 'fast')) {
362
+ modelSettings[key] = { ...current, fast: true };
363
+ }
364
+ }
365
+ return modelSettings;
366
+ }
367
+
368
+ function canonicalizeLegacyAgentStorage(value = {}) {
369
+ const next = canonicalizeAgentRouteStorage(value);
370
+ next.presets = Array.isArray(next.presets)
371
+ ? next.presets.map((preset) => normalizePreset(preset)).filter(Boolean)
372
+ : [];
373
+ next.modelSettings = migrateLegacyModelSettings(value);
374
+ const autoClear = canonicalizeAutoClearStorage(next.autoClear);
375
+ if (autoClear) next.autoClear = autoClear;
376
+ else delete next.autoClear;
377
+ const compaction = canonicalizeCompactionStorage(next.compaction);
378
+ if (compaction) next.compaction = compaction;
379
+ else delete next.compaction;
380
+ const shell = canonicalizeShellStorage(next.shell);
381
+ if (shell) next.shell = shell;
382
+ else delete next.shell;
383
+ if (Object.prototype.hasOwnProperty.call(next, 'profile')) {
384
+ next.profile = normalizeProfileConfig(next.profile);
385
+ }
386
+ if (Object.prototype.hasOwnProperty.call(next, 'skills')) {
387
+ const skills = normalizeSkillsConfig(next.skills);
388
+ if (skills.disabled.length) next.skills = skills;
389
+ else delete next.skills;
390
+ }
391
+ if (!next.searchRoute && value?.search && typeof value.search === 'object') {
392
+ next.searchRoute = value.search;
393
+ }
394
+ next.searchRoute = normalizeSearchRoute(next.searchRoute);
395
+ const modules = next.modules && typeof next.modules === 'object' && !Array.isArray(next.modules)
396
+ ? { ...next.modules }
397
+ : {};
398
+ if (Object.prototype.hasOwnProperty.call(modules, 'memory')) {
399
+ const legacyMemory = modules.memory;
400
+ if (!(next.recap && typeof next.recap === 'object' && Object.prototype.hasOwnProperty.call(next.recap, 'enabled'))) {
401
+ const enabled = legacyMemory && typeof legacyMemory === 'object'
402
+ ? legacyMemory.enabled !== false
403
+ : legacyMemory !== false;
404
+ next.recap = { ...(next.recap || {}), enabled };
405
+ }
406
+ delete modules.memory;
407
+ }
408
+ if (Object.keys(modules).length) next.modules = modules;
409
+ else delete next.modules;
410
+ delete next.fastModels;
411
+ delete next.agentMaintenance;
412
+ delete next.runtime;
413
+ delete next.search;
414
+ delete next.capabilities;
415
+ delete next.defaultProvider;
416
+ delete next.guide;
417
+ return next;
418
+ }
419
+
420
+ function agentConfigStorageNeedsMigration(value = {}) {
421
+ const modules = value?.modules;
422
+ const presets = Array.isArray(value?.presets) ? value.presets : [];
423
+ const canonical = canonicalizeLegacyAgentStorage(value);
424
+ const normalizedFields = ['autoClear', 'compaction', 'shell', 'profile', 'skills', 'modules', 'guide'];
425
+ return agentRouteStorageNeedsMigration(value)
426
+ || Object.prototype.hasOwnProperty.call(value || {}, 'fastModels')
427
+ || Object.prototype.hasOwnProperty.call(value || {}, 'agentMaintenance')
428
+ || Object.prototype.hasOwnProperty.call(value || {}, 'runtime')
429
+ || Object.prototype.hasOwnProperty.call(value || {}, 'search')
430
+ || Object.prototype.hasOwnProperty.call(value || {}, 'capabilities')
431
+ || Object.prototype.hasOwnProperty.call(value || {}, 'defaultProvider')
432
+ || presets.some((preset) => !normalizePreset(preset))
433
+ || (modules && typeof modules === 'object' && Object.prototype.hasOwnProperty.call(modules, 'memory'))
434
+ || normalizedFields.some((key) => JSON.stringify(value?.[key]) !== JSON.stringify(canonical?.[key]));
435
+ }
436
+
284
437
  export function loadConfig(options = {}) {
285
438
  const includeSecrets = options.secrets !== false;
286
439
  const sectionRaw = readSection('agent');
@@ -290,6 +443,8 @@ export function loadConfig(options = {}) {
290
443
  if (raw.agent && raw.agent.providers) {
291
444
  raw = raw.agent;
292
445
  }
446
+ const storageNeedsMigration = agentConfigStorageNeedsMigration(raw);
447
+ raw = canonicalizeLegacyAgentStorage(raw);
293
448
  const defaults = buildDefaultConfig({ detectCredentials: includeSecrets });
294
449
  // Deep-merge provider subkeys: unknown per-provider values are
295
450
  // preserved through save/load so future fields round-trip
@@ -371,27 +526,13 @@ export function loadConfig(options = {}) {
371
526
  // without overriding an explicit modelSettings.fast value. Keep
372
527
  // the compatibility read here, but never expose/persist the
373
528
  // legacy container again.
374
- const modelSettings = raw.modelSettings && typeof raw.modelSettings === 'object'
375
- ? { ...raw.modelSettings }
376
- : {};
377
- const legacyFastModels = raw.fastModels && typeof raw.fastModels === 'object'
378
- ? raw.fastModels
379
- : {};
380
- for (const [key, enabled] of Object.entries(legacyFastModels)) {
381
- if (enabled !== true) continue;
382
- const current = modelSettings[key];
383
- if (!current || typeof current !== 'object' || Array.isArray(current)) {
384
- modelSettings[key] = { fast: true };
385
- } else if (!Object.prototype.hasOwnProperty.call(current, 'fast')) {
386
- modelSettings[key] = { ...current, fast: true };
387
- }
388
- }
529
+ const modelSettings = migrateLegacyModelSettings(raw);
389
530
  const workflowRoutes = raw.workflowRoutes && typeof raw.workflowRoutes === 'object' ? { ...raw.workflowRoutes } : {};
390
531
  delete workflowRoutes.search;
391
532
  // Normalize maintenance slots to routes, then overlay onto the
392
533
  // route-shaped defaults.
393
- const normalizedMaint = normalizeMaintenanceRoutes(rawMaint, raw.defaultProvider);
394
- return {
534
+ const normalizedMaint = normalizeMaintenanceRoutes(rawMaint);
535
+ const loaded = canonicalizeAgentRouteStorage({
395
536
  providers: mergedProviders,
396
537
  mcpServers,
397
538
  mcpProjectOverrides: raw.mcpProjectOverrides && typeof raw.mcpProjectOverrides === 'object' && !Array.isArray(raw.mcpProjectOverrides)
@@ -418,7 +559,40 @@ export function loadConfig(options = {}) {
418
559
  update: raw.update && typeof raw.update === 'object' ? { ...raw.update } : {},
419
560
  recap: recapConfig,
420
561
  modules: raw.modules && typeof raw.modules === 'object' ? { ...raw.modules } : {},
421
- };
562
+ });
563
+ if (storageNeedsMigration) {
564
+ try {
565
+ // `readSection('agent')` sees cross-section migrations from
566
+ // canonicalizeUnifiedConfig (root autoClear/compaction/
567
+ // shell and memory.user.title), while the in-lock raw
568
+ // section does not. Seed only those missing/partial values
569
+ // from the canonical read snapshot; the current in-lock
570
+ // section still wins for concurrent edits.
571
+ persistAgentConfig((current) => {
572
+ const merged = { ...current };
573
+ for (const key of ['autoClear', 'compaction', 'shell', 'recap']) {
574
+ if (!Object.prototype.hasOwnProperty.call(current, key)
575
+ && Object.prototype.hasOwnProperty.call(raw, key)) {
576
+ merged[key] = raw[key];
577
+ }
578
+ }
579
+ if (Object.prototype.hasOwnProperty.call(raw, 'profile')) {
580
+ const currentProfile = normalizeProfileConfig(current.profile);
581
+ const migratedProfile = normalizeProfileConfig(raw.profile);
582
+ merged.profile = {
583
+ ...currentProfile,
584
+ ...(!currentProfile.title && migratedProfile.title
585
+ ? { title: migratedProfile.title }
586
+ : {}),
587
+ };
588
+ }
589
+ return canonicalizeLegacyAgentStorage(merged);
590
+ });
591
+ } catch (err) {
592
+ process.stderr.write(`[config] persist canonical agent config failed: ${err?.message}\n`);
593
+ }
594
+ }
595
+ return loaded;
422
596
  }
423
597
  catch { /* fall through */ }
424
598
  }
@@ -430,7 +604,6 @@ export function loadConfig(options = {}) {
430
604
  presets: DEFAULT_PRESETS.map(p => ({ ...p })),
431
605
  default: null,
432
606
  maintenance: { ...DEFAULT_MAINTENANCE },
433
- workflowRoutes: {},
434
607
  searchRoute: null,
435
608
  modelSettings: {},
436
609
  onboarding: {},
@@ -491,6 +664,7 @@ export async function patchSkillsDisabledAsync(disabledNames) {
491
664
  }
492
665
 
493
666
  function buildAgentSaveBuilder(config, appliedDirty) {
667
+ const canonicalRoutes = canonicalizeLegacyAgentStorage(config);
494
668
  // Strip ephemeral defaults from providers but preserve any unknown
495
669
  // per-provider subkey so future schema additions round-trip through the
496
670
  // setup UI without changes here. apiKey is intentionally omitted —
@@ -514,13 +688,15 @@ function buildAgentSaveBuilder(config, appliedDirty) {
514
688
  persistedProviders[name] = slim;
515
689
  }
516
690
  }
517
- const workflowRoutes = config.workflowRoutes && typeof config.workflowRoutes === 'object'
518
- ? { ...config.workflowRoutes }
519
- : {};
520
- delete workflowRoutes.search;
521
- const presets = Array.isArray(config.presets)
522
- ? config.presets.filter(p => p?.id !== 'workflow-search')
691
+ const presets = Array.isArray(canonicalRoutes.presets)
692
+ ? canonicalRoutes.presets.filter(p => p?.id !== 'workflow-search')
523
693
  : [];
694
+ const profile = normalizeProfileConfig(config.profile);
695
+ const skills = normalizeSkillsConfig(config.skills);
696
+ const autoClear = canonicalizeAutoClearStorage(config.autoClear);
697
+ const compaction = canonicalizeCompactionStorage(config.compaction);
698
+ const shell = canonicalizeShellStorage(config.shell);
699
+ const modules = canonicalizeModulesStorage(config.modules);
524
700
  // Build the replacement from `existingRaw` — the section read INSIDE the
525
701
  // file lock — not a snapshot taken before it, so unmanaged keys written by
526
702
  // a concurrent instance survive the save (lost-update guard).
@@ -559,27 +735,25 @@ function buildAgentSaveBuilder(config, appliedDirty) {
559
735
  }
560
736
  const next = {
561
737
  ...existingRaw,
562
- guide: config.guide || existingRaw.guide || undefined,
563
738
  providers: persistedProviders,
564
739
  mcpServers,
565
740
  mcpProjectOverrides,
566
741
  presets,
567
742
  default: config.default || null,
568
- maintenance: config.maintenance || {},
569
- workflowRoutes,
743
+ maintenance: canonicalRoutes.maintenance,
570
744
  searchRoute: normalizeSearchRoute(config.searchRoute),
571
745
  modelSettings: config.modelSettings || {},
572
746
  onboarding: config.onboarding || {},
573
- agents: config.agents || {},
747
+ agents: canonicalRoutes.agents,
574
748
  workflow: config.workflow || { active: 'default' },
575
- profile: normalizeProfileConfig(config.profile),
576
- skills: normalizeSkillsConfig(config.skills),
577
- autoClear: config.autoClear || {},
578
- compaction: config.compaction || {},
579
- shell: config.shell || {},
749
+ profile,
750
+ skills: skills.disabled.length ? skills : undefined,
751
+ autoClear,
752
+ compaction,
753
+ shell,
580
754
  update: config.update || {},
581
755
  recap: config.recap || {},
582
- modules: config.modules || existingRaw.modules || {},
756
+ modules,
583
757
  };
584
758
  // These keys were previously round-tripped despite having no runtime
585
759
  // consumer. Explicitly remove them from the in-lock baseline so a
@@ -587,6 +761,11 @@ function buildAgentSaveBuilder(config, appliedDirty) {
587
761
  delete next.fastModels;
588
762
  delete next.agentMaintenance;
589
763
  delete next.runtime;
764
+ delete next.workflowRoutes;
765
+ delete next.search;
766
+ delete next.capabilities;
767
+ delete next.defaultProvider;
768
+ delete next.guide;
590
769
  return next;
591
770
  };
592
771
  }
@@ -8,6 +8,9 @@ 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';
12
+ import { createOwnerFairGate } from '../../../shared/owner-fair-gate.mjs';
13
+ import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.mjs';
11
14
  // --- Types ---
12
15
  /** Known auto-detect targets: port file path relative to tmpdir.
13
16
  * Note: `mixdog` used to self-loopback via active-instance.json's
@@ -16,13 +19,15 @@ import { classifyResultKind } from '../session/result-classification.mjs';
16
19
  * in-process through agent's toolExecutor (see orchestrator/internal-tools),
17
20
  * so this registry is for genuinely external port-based MCP targets only. */
18
21
  const AUTO_DETECT_PORTS = {
19
- 'mixdog-memory': { discovery: 'memory', dir: 'mixdog', file: 'active-instance.json', portField: 'memory_port', endpoint: '/mcp' },
22
+ 'mixdog-memory': { discovery: 'memory', endpoint: '/mcp' },
20
23
  };
21
24
  const DEFAULT_MCP_CALL_TIMEOUT_MS = 120000;
22
25
  // Per-server STARTUP handshake budget (connect + listTools): 10s.
23
26
  const DEFAULT_MCP_STARTUP_TIMEOUT_MS = 10000;
24
27
  // --- State ---
25
28
  const servers = new Map();
29
+ const reconnects = createKeyedSingleflight();
30
+ const callAdmissions = new Map();
26
31
  let mcpSdkPromise = null;
27
32
  // Memo for mcpToolHasField(name, field) — keyed by `${toolName}|${field}`.
28
33
  // The lookup (regex parse + servers Map get + tools.find + schema property
@@ -163,6 +168,45 @@ export function getMcpServerStatus() {
163
168
  })(),
164
169
  }));
165
170
  }
171
+ function positiveInt(value, fallback) {
172
+ const parsed = Math.floor(Number(value));
173
+ return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
174
+ }
175
+ function callAdmissionFor(server) {
176
+ let gate = callAdmissions.get(server.name);
177
+ if (gate) return gate;
178
+ const cfg = server?.cfg || {};
179
+ gate = createOwnerFairGate({
180
+ name: `MCP ${server.name}`,
181
+ activeMax: positiveInt(
182
+ cfg.maxConcurrency ?? cfg.max_concurrency ?? process.env.MIXDOG_MCP_MAX_INFLIGHT,
183
+ 8,
184
+ ),
185
+ queueMax: positiveInt(
186
+ cfg.maxQueue ?? cfg.max_queue ?? process.env.MIXDOG_MCP_MAX_QUEUE,
187
+ 256,
188
+ ),
189
+ minOwnerQueue: 8,
190
+ waitTimeoutMs: positiveInt(
191
+ cfg.waitTimeoutMs ?? cfg.wait_timeout_ms ?? process.env.MIXDOG_MCP_WAIT_TIMEOUT_MS,
192
+ 30_000,
193
+ ),
194
+ });
195
+ callAdmissions.set(server.name, gate);
196
+ return gate;
197
+ }
198
+ function closeCallAdmission(name, reason) {
199
+ const gate = callAdmissions.get(name);
200
+ if (!gate) return;
201
+ callAdmissions.delete(name);
202
+ gate.close(reason || `MCP ${name} disconnected`);
203
+ }
204
+ export function getMcpAdmissionSnapshot() {
205
+ return [...callAdmissions.entries()].map(([name, gate]) => ({
206
+ name,
207
+ ...gate.snapshot(),
208
+ }));
209
+ }
166
210
 
167
211
  /** Snapshot of MCP initialize `instructions` per connected server (handshake time). */
168
212
  export function getMcpServerInstructionsMap() {
@@ -173,11 +217,31 @@ export function getMcpServerInstructionsMap() {
173
217
  }
174
218
  return out;
175
219
  }
220
+
221
+ async function reconnectMcpServer(serverName, failedServer) {
222
+ return reconnects.run(serverName, async () => {
223
+ const current = servers.get(serverName);
224
+ // A peer already completed the replacement while this failed call was
225
+ // unwinding. Reuse it immediately instead of closing a fresh transport.
226
+ if (current && current !== failedServer) return current;
227
+ if (current) {
228
+ await _closeServer(current);
229
+ if (servers.get(serverName) === current) servers.delete(serverName);
230
+ }
231
+ await connectServer(serverName, failedServer.cfg);
232
+ const replacement = servers.get(serverName);
233
+ if (!replacement) {
234
+ throw new Error(`reconnect succeeded but server "${serverName}" entry is missing from registry`);
235
+ }
236
+ return replacement;
237
+ });
238
+ }
239
+
176
240
  /**
177
241
  * Execute an MCP tool call.
178
242
  * Name format: `mcp__{serverName}__{toolName}`
179
243
  */
180
- export async function executeMcpTool(name, args) {
244
+ export async function executeMcpTool(name, args, options = {}) {
181
245
  // Parse: mcp__{server}__{tool}
182
246
  const match = name.match(/^mcp__(.+?)__(.+)$/);
183
247
  if (!match)
@@ -186,42 +250,40 @@ export async function executeMcpTool(name, args) {
186
250
  const server = servers.get(serverName);
187
251
  if (!server)
188
252
  throw new Error(`MCP server "${serverName}" not connected`);
189
- let result;
190
- try {
191
- result = await _callToolWithTimeout(server, toolName, args);
192
- } catch (firstErr) {
193
- const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
194
- if (isMcpToolCallTimeoutError(firstErr)) {
195
- mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
196
- throw firstErr;
197
- }
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 */ }
253
+ const gate = callAdmissionFor(server);
254
+ return gate.run(options?.ownerKey || currentToolExecutionOwner(), async () => {
255
+ let result;
203
256
  try {
204
- await connectServer(serverName, server.cfg);
205
- } catch (reconnectErr) {
206
- const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
207
- throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
208
- }
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
- try {
214
- result = await _callToolWithTimeout(retryServer, toolName, args);
215
- } catch (retryErr) {
216
- const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
217
- throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
257
+ result = await _callToolWithTimeout(server, toolName, args);
258
+ } catch (firstErr) {
259
+ const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
260
+ if (isMcpToolCallTimeoutError(firstErr)) {
261
+ mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
262
+ throw firstErr;
263
+ }
264
+ mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
265
+ let retryServer;
266
+ try {
267
+ retryServer = await reconnectMcpServer(serverName, server);
268
+ } catch (reconnectErr) {
269
+ const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
270
+ throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
271
+ }
272
+ try {
273
+ result = await _callToolWithTimeout(retryServer, toolName, args);
274
+ } catch (retryErr) {
275
+ const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
276
+ throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
277
+ }
218
278
  }
219
- }
220
- const normalized = normalizeToolEnvelope(normalizeMcpToolResult(result));
221
- const text = capMcpOutput(normalized.result);
222
- return normalized.explicitSuccess
223
- ? makeToolEnvelope(text, [], { explicitSuccess: true })
224
- : text;
279
+ const normalized = normalizeToolEnvelope(normalizeMcpToolResult(result));
280
+ const text = capMcpOutput(normalized.result);
281
+ return normalized.explicitSuccess
282
+ ? makeToolEnvelope(text, [], { explicitSuccess: true })
283
+ : text;
284
+ }, {
285
+ signal: options?.signal || null,
286
+ });
225
287
  }
226
288
 
227
289
  // Preserve MCP failure metadata across the object→string boundary. The
@@ -410,6 +472,7 @@ export async function disconnectAll() {
410
472
  }
411
473
  catch { /* ignore */ }
412
474
  servers.delete(name);
475
+ closeCallAdmission(name, `MCP ${name} disconnected`);
413
476
  }
414
477
  _invalidateMcpToolFieldMemo();
415
478
  }
@@ -427,6 +490,7 @@ export async function disconnectMcpServer(name) {
427
490
  }
428
491
  catch { /* ignore */ }
429
492
  servers.delete(name);
493
+ closeCallAdmission(name, `MCP ${name} disconnected`);
430
494
  _invalidateMcpToolFieldMemo();
431
495
  return true;
432
496
  }
@@ -479,13 +543,11 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
479
543
  const spec = AUTO_DETECT_PORTS[cfg.autoDetect];
480
544
  if (!spec)
481
545
  throw new Error(`Unknown autoDetect target: "${cfg.autoDetect}"`);
482
- // Prefer the single-writer discovery advert (discovery/<service>.json),
483
- // pid-validated. Fall back to the legacy active-instance.json portField
484
- // when no live advert is present (cross-version compat).
546
+ // Read the pid-validated single-writer discovery advert.
485
547
  let port = spec.discovery ? readServicePort(spec.discovery, { requirePid: false }) : null;
486
548
  if (port && spec.discovery) _autoDetectAdvert = { service: spec.discovery, port };
487
549
  let portFile = null;
488
- if (!port) {
550
+ if (!port && spec.file) {
489
551
  portFile = spec.dir === 'mixdog' && process.env.MIXDOG_RUNTIME_ROOT
490
552
  ? join(process.env.MIXDOG_RUNTIME_ROOT, spec.file)
491
553
  : join(tmpdir(), spec.dir, spec.file);
@@ -510,6 +572,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
510
572
  port = parseInt(raw, 10);
511
573
  }
512
574
  }
575
+ if (!port) throw new Error(`autoDetect server "${name}": live service advert missing`);
513
576
  if (!Number.isFinite(port) || port < 1 || port > 65535) {
514
577
  throw new Error(`autoDetect server "${name}": invalid port value${portFile ? ` in ${portFile}` : ''}`);
515
578
  }
@@ -632,7 +695,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
632
695
  ...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
633
696
  }));
634
697
  const toolNames = tools.map(t => t.name);
635
- servers.set(name, { name, client, transport, tools, cfg, instructions });
698
+ servers.set(name, { name, client, transport, tools, cfg, instructions, generation: genAtStart });
636
699
  _invalidateMcpToolFieldMemo();
637
700
  mcpLog(`[mcp] connected: ${tools.length} tools — ${toolNames.join(', ')}\n`);
638
701
  }