mixdog 1.1.2 → 1.2.1

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 (424) hide show
  1. package/README.md +8 -5
  2. package/package.json +2 -1
  3. package/scripts/lib/run-node-tests.mjs +9 -0
  4. package/scripts/prune-desktop-runtime.mjs +3 -0
  5. package/scripts/test-direct.mjs +4 -2
  6. package/src/defaults/shipped-history.json +10 -0
  7. package/src/defaults/skills/pptx/SKILL.md +6 -6
  8. package/src/defaults/skills/pptx/references/review.md +6 -4
  9. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  10. package/src/defaults/skills/setup/references/actions.md +18 -5
  11. package/src/defaults/skills/visualize/SKILL.md +160 -0
  12. package/src/defaults/skills/visualize/references/craft.md +88 -0
  13. package/src/headless-command.mjs +56 -3
  14. package/src/headless-exec.mjs +17 -1
  15. package/src/help.mjs +2 -0
  16. package/src/rules/shared/60-verification.md +3 -1
  17. package/src/rules/shared/70-delivery.md +4 -2
  18. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  19. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  21. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  22. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  23. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  24. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  25. package/src/runtime/agent/orchestrator/config.mjs +19 -5
  26. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  27. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  28. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  29. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +133 -15
  30. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +55 -8
  31. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +86 -8
  32. package/src/runtime/agent/orchestrator/mcp/client.mjs +474 -164
  33. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  34. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  35. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  36. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -12
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  43. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  44. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  45. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  46. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  47. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  50. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +21 -6
  51. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  52. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  53. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  56. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  57. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  59. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  60. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  61. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +82 -0
  62. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +556 -0
  63. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  64. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  66. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +15 -0
  67. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +15 -4
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -7
  78. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  79. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  80. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -11
  81. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  82. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  83. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  84. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +278 -84
  85. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  86. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  87. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  88. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
  89. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  90. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  91. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  92. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  93. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  94. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  95. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  96. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  97. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +24 -13
  98. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  99. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  100. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  101. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  102. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  103. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  104. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  105. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  106. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  107. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  108. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  109. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  112. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  114. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +5 -3
  115. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -4
  116. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +21 -10
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  119. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -3
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +22 -12
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +9 -3
  124. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  125. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  126. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  128. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  129. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  130. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  131. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  132. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  133. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  136. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  137. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  138. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +33 -22
  139. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  140. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  141. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  142. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  143. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  144. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  145. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +36 -0
  146. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +23 -12
  147. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  148. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +202 -374
  149. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  150. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  151. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  152. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +48 -9
  153. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +161 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  155. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  157. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  158. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  159. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  160. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  161. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  162. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +203 -33
  163. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  164. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  165. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  166. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  167. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -68
  168. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  169. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +20 -3
  170. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  171. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  175. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  176. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  177. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  178. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  179. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  180. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  181. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  182. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  183. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  184. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  185. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  186. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  187. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  188. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  189. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  190. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  191. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  192. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  193. package/src/runtime/attachments/store.d.mts +3 -0
  194. package/src/runtime/attachments/store.mjs +38 -0
  195. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  196. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  197. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  198. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  199. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  200. package/src/runtime/memory/lib/memory.mjs +1 -1
  201. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  202. package/src/runtime/shared/atomic-file.mjs +33 -6
  203. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  204. package/src/runtime/shared/code-graph-state.mjs +12 -0
  205. package/src/runtime/shared/defaults-separation.mjs +2 -0
  206. package/src/runtime/shared/extension-scopes.mjs +5 -3
  207. package/src/runtime/shared/file-lock.mjs +37 -11
  208. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  209. package/src/runtime/shared/git-runner.mjs +4 -3
  210. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  211. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  212. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  213. package/src/runtime/shared/llm/model-catalog.mjs +240 -130
  214. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  215. package/src/runtime/shared/llm/provider-catalog-registry.mjs +162 -0
  216. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  217. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  218. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  219. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  220. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  221. package/src/runtime/shared/pristine-execution.mjs +28 -4
  222. package/src/runtime/shared/provider-accounts.mjs +125 -21
  223. package/src/runtime/shared/provider-api-key.mjs +33 -1
  224. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  225. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  226. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  227. package/src/runtime/shared/shell-display.mjs +9 -3
  228. package/src/runtime/shared/singleton-owner.mjs +36 -6
  229. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  230. package/src/runtime/shared/skill-mentions.mjs +133 -0
  231. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  232. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  233. package/src/runtime/shared/tool-primitives.mjs +35 -2
  234. package/src/runtime/shared/tool-surface.mjs +7 -0
  235. package/src/runtime/shared/tool-work-units.mjs +14 -0
  236. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  237. package/src/runtime/shared/turn-changes.mjs +791 -0
  238. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  239. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  240. package/src/runtime/shared/turn-worktree-snapshot.mjs +185 -15
  241. package/src/runtime/shared/worker-requests.mjs +15 -2
  242. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  243. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  244. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  245. package/src/runtime/shared/worktrees/git.mjs +247 -0
  246. package/src/runtime/shared/worktrees/include.mjs +480 -0
  247. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  248. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  249. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  250. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  251. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  252. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  253. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  254. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  255. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  256. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  257. package/src/runtime/visualize-bridge/client.mjs +181 -0
  258. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  259. package/src/session-runtime/automation-agents.mjs +8 -2
  260. package/src/session-runtime/boot/apis.mjs +90 -0
  261. package/src/session-runtime/boot/config.mjs +7 -2
  262. package/src/session-runtime/boot/facade.mjs +21 -0
  263. package/src/session-runtime/boot/infrastructure.mjs +30 -18
  264. package/src/session-runtime/boot/session.mjs +1 -0
  265. package/src/session-runtime/boot/tools.mjs +4 -0
  266. package/src/session-runtime/config-lifecycle.mjs +10 -1
  267. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  268. package/src/session-runtime/conversation-import/service.mjs +369 -0
  269. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +21 -6
  270. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  271. package/src/session-runtime/global-extensions.mjs +2 -2
  272. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  273. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  274. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  275. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  276. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  277. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  278. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  279. package/src/session-runtime/mcp-glue/connect.mjs +68 -16
  280. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  281. package/src/session-runtime/mcp-glue/server-catalog.mjs +122 -33
  282. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  283. package/src/session-runtime/mcp-glue.mjs +22 -2
  284. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  285. package/src/session-runtime/model-route/set-route.mjs +15 -2
  286. package/src/session-runtime/new-session-config.mjs +3 -2
  287. package/src/session-runtime/notification-bus.mjs +18 -3
  288. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  289. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  290. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  291. package/src/session-runtime/provider-readiness.mjs +16 -10
  292. package/src/session-runtime/resource-extension-sync.mjs +45 -7
  293. package/src/session-runtime/resource-mcp-api.mjs +121 -84
  294. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  295. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  296. package/src/session-runtime/resource-skills-api.mjs +23 -2
  297. package/src/session-runtime/route-resolution.mjs +10 -1
  298. package/src/session-runtime/route-state.mjs +16 -2
  299. package/src/session-runtime/runtime-core.mjs +5 -0
  300. package/src/session-runtime/runtime-facade.mjs +8 -0
  301. package/src/session-runtime/runtime-review-api.mjs +9 -0
  302. package/src/session-runtime/schedule-session-run.mjs +1 -1
  303. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  304. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  305. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  306. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  307. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  308. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  309. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  310. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  311. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  312. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  313. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  314. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  315. package/src/session-runtime/services/agent-tool.mjs +1 -0
  316. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  317. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  318. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  319. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  320. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  321. package/src/session-runtime/services/provider-admin.mjs +64 -18
  322. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  323. package/src/session-runtime/session-create/session-options.mjs +10 -1
  324. package/src/session-runtime/session-turn-api.mjs +1 -0
  325. package/src/session-runtime/settings-api.mjs +4 -2
  326. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  327. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  328. package/src/session-runtime/setup-tool/executor.mjs +23 -5
  329. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  330. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -1
  331. package/src/session-runtime/setup-tool/tool-defs.mjs +38 -4
  332. package/src/session-runtime/skills-api.mjs +7 -3
  333. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  334. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +14 -3
  335. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  336. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  337. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  338. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  339. package/src/session-runtime/turn/turn-run.mjs +6 -7
  340. package/src/session-runtime/webhook-session-run.mjs +16 -4
  341. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  342. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  343. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  344. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  345. package/src/session-runtime/workspace/transitions.mjs +651 -0
  346. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  347. package/src/skill-script-entry.mjs +36 -0
  348. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  349. package/src/standalone/channel-transport.mjs +13 -1
  350. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  351. package/src/standalone/daemon-owner.mjs +372 -0
  352. package/src/standalone/daemon-stored-session-views.mjs +15 -4
  353. package/src/standalone/daemon.mjs +179 -8
  354. package/src/standalone/desktop-service-registry.mjs +20 -0
  355. package/src/standalone/local-session-runtime.mjs +11 -0
  356. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  357. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  358. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  359. package/src/standalone/session-client.mjs +104 -13
  360. package/src/standalone/session-protocol.mjs +21 -0
  361. package/src/standalone/session-proxy.mjs +3 -0
  362. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  363. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  364. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  365. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  366. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  367. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  368. package/src/standalone/session-service/session-calls/session-action.mjs +17 -4
  369. package/src/standalone/session-transport/call-route.mjs +13 -0
  370. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  371. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  372. package/src/standalone/session-transport/client-registry.mjs +20 -3
  373. package/src/standalone/session-transport/routes.mjs +79 -2
  374. package/src/standalone/session-transport/stack.mjs +2 -0
  375. package/src/standalone/session-transport.mjs +24 -1
  376. package/src/tui/App.jsx +9 -1
  377. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  378. package/src/tui/app/app-view.jsx +6 -0
  379. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  380. package/src/tui/app/message-selector.mjs +2 -1
  381. package/src/tui/app/model-options.mjs +37 -5
  382. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  383. package/src/tui/app/model-picker.mjs +4 -2
  384. package/src/tui/app/skill-mention-palette.mjs +88 -0
  385. package/src/tui/app/slash-palette.mjs +23 -7
  386. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  387. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  388. package/src/tui/app/use-prompt-state.mjs +5 -0
  389. package/src/tui/components/Message.jsx +22 -1
  390. package/src/tui/components/PromptInput.jsx +21 -3
  391. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  392. package/src/tui/components/TranscriptItem.jsx +9 -1
  393. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  394. package/src/tui/components/user-message-skills.mjs +38 -0
  395. package/src/tui/dist/index.mjs +839 -207
  396. package/src/tui/index.jsx +10 -2
  397. package/src/tui/session/compaction-view.mjs +147 -0
  398. package/src/tui/session/context-state.mjs +6 -0
  399. package/src/tui/session/initial-state.mjs +3 -0
  400. package/src/tui/session/queue-helpers.mjs +7 -2
  401. package/src/tui/session/quota-resume.mjs +2 -1
  402. package/src/tui/session/session-api/commands.mjs +10 -2
  403. package/src/tui/session/session-api/controls.mjs +4 -0
  404. package/src/tui/session/session-api/extensions.mjs +4 -2
  405. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  406. package/src/tui/session/session-api/integrations.mjs +17 -0
  407. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  408. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  409. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  410. package/src/tui/session/session-api/routes.mjs +10 -1
  411. package/src/tui/session/session-api/settings.mjs +9 -0
  412. package/src/tui/session/session-api/worktree.mjs +60 -0
  413. package/src/tui/session/session-api.mjs +2 -0
  414. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  415. package/src/tui/session/session-flow/drain.mjs +27 -3
  416. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  417. package/src/tui/session/session-flow.mjs +2 -0
  418. package/src/tui/session/transcript-intake.mjs +14 -5
  419. package/src/tui/session/tui-steering-persist.mjs +25 -9
  420. package/src/tui/session/turn-stream.mjs +32 -4
  421. package/src/tui/session/turn-tool-cards.mjs +10 -2
  422. package/src/tui/session/turn.mjs +56 -10
  423. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  424. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -22,9 +22,15 @@ import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
22
22
  import { createOwnerFairGate } from '../../../shared/owner-fair-gate.mjs';
23
23
  import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.mjs';
24
24
  import { positiveInt } from '../../../shared/numbers.mjs';
25
- import { resolveMcpCallTimeoutMs, resolveMcpTransportKind, scrubMcpConnectionMessage } from './client-config.mjs';
26
- import { createMcpTransport } from './client-transport.mjs';
27
- import { runBoundedHandshake } from './client-handshake.mjs';
25
+ import {
26
+ providerSafeMcpToolName,
27
+ resolveMcpRequestTimeouts,
28
+ resolveMcpTransportKind,
29
+ scrubMcpConnectionMessage,
30
+ } from './client-config.mjs';
31
+ import { createLazyOutputValidator } from './lazy-output-validator.mjs';
32
+ import { appendMcpStderrTail, createMcpTransport } from './client-transport.mjs';
33
+ import { listAllMcpTools, runBoundedHandshake } from './client-handshake.mjs';
28
34
 
29
35
  // Config interpretation (transport kind, URL policy, env expansion, headers,
30
36
  // timeouts, log scrubbing) lives in client-config.mjs; re-exported so every
@@ -37,6 +43,9 @@ export {
37
43
  resolveMcpHttpHeaders,
38
44
  resolveMcpTransportKind,
39
45
  resolveMcpStartupTimeoutMs,
46
+ resolveMcpCallTimeoutMs,
47
+ resolveMcpMaxTotalTimeoutMs,
48
+ resolveMcpRequestTimeouts,
40
49
  } from './client-config.mjs';
41
50
  const servers = new Map();
42
51
  const reconnects = createKeyedSingleflight();
@@ -47,6 +56,8 @@ const MCP_TOOL_NAME_RE = /^mcp__(.+?)__(.+)$/;
47
56
  const _knownMcpScopes = new Set([DEFAULT_MCP_SCOPE_ID]);
48
57
  const _connectAbortGenerations = new Map();
49
58
  const _pendingConnects = new Set();
59
+ // scopeId -> root path advertised by that scope's scope-following servers.
60
+ const _scopeRoots = new Map();
50
61
  let mcpSdkPromise = null;
51
62
  // Memo for mcpToolHasField(name, field) — keyed by `${scopeId}|${toolName}|${field}`.
52
63
  // The lookup (regex parse + servers Map get + tools.find + schema property
@@ -55,8 +66,14 @@ let mcpSdkPromise = null;
55
66
  // mutation point (connectServer / disconnectAll) so a stale positive or
56
67
  // negative can never survive a tools-list change.
57
68
  const _mcpToolFieldMemo = new Map();
69
+ let _mcpToolGeneration = 0;
58
70
  function _invalidateMcpToolFieldMemo() {
59
71
  _mcpToolFieldMemo.clear();
72
+ _mcpToolGeneration += 1;
73
+ }
74
+ /** Read-only counter bumped on every MCP tool registry mutation. */
75
+ export function getMcpToolGeneration() {
76
+ return _mcpToolGeneration;
60
77
  }
61
78
  function normalizeMcpScopeId(value) {
62
79
  const raw = value && typeof value === 'object' ? value.scopeId : value;
@@ -95,12 +112,15 @@ async function loadMcpSdk() {
95
112
  import('@modelcontextprotocol/sdk/client/sse.js'),
96
113
  import('@modelcontextprotocol/sdk/client/websocket.js'),
97
114
  import('@modelcontextprotocol/sdk/types.js'),
98
- ]).then(([clientMod, stdioMod, httpMod, sseMod, wsMod, typesMod]) => ({
115
+ import('@modelcontextprotocol/sdk/validation/ajv'),
116
+ ]).then(([clientMod, stdioMod, httpMod, sseMod, wsMod, typesMod, ajvMod]) => ({
99
117
  Client: clientMod.Client,
118
+ AjvJsonSchemaValidator: ajvMod.AjvJsonSchemaValidator,
100
119
  StdioClientTransport: stdioMod.StdioClientTransport,
101
120
  StreamableHTTPClientTransport: httpMod.StreamableHTTPClientTransport,
102
121
  SSEClientTransport: sseMod.SSEClientTransport,
103
122
  WebSocketClientTransport: wsMod.WebSocketClientTransport,
123
+ JSONRPCMessageSchema: typesMod.JSONRPCMessageSchema,
104
124
  ListRootsRequestSchema: typesMod.ListRootsRequestSchema,
105
125
  ToolListChangedNotificationSchema: typesMod.ToolListChangedNotificationSchema,
106
126
  }));
@@ -160,8 +180,8 @@ function mcpFeatureTools(serverName, capabilities, protocolTools) {
160
180
  const tools = [];
161
181
  const used = new Set(protocolTools.map((tool) => tool.name));
162
182
  const add = (leaf, description, properties, required, operation) => {
163
- let name = `mcp__${serverName}__${leaf}`;
164
- while (used.has(name)) name += '_';
183
+ let name = providerSafeMcpToolName(serverName, leaf);
184
+ for (let suffix = '_'; used.has(name); suffix += '_') name = providerSafeMcpToolName(serverName, leaf + suffix);
165
185
  used.add(name);
166
186
  tools.push({
167
187
  name,
@@ -226,7 +246,16 @@ function mcpFeatureTools(serverName, capabilities, protocolTools) {
226
246
  export function getMcpServerStatus(scopeId = DEFAULT_MCP_SCOPE_ID) {
227
247
  return scopedServerEntries(scopeId).map(([, server]) => ({
228
248
  name: server.name,
229
- connected: true,
249
+ connected: server.liveness?.alive !== false,
250
+ ...(server.liveness?.alive === false
251
+ ? {
252
+ error: appendMcpStderrTail(
253
+ server.liveness.lastError || `MCP server "${server.name}" closed`,
254
+ server.stderrTail?.()
255
+ ),
256
+ }
257
+ : {}),
258
+ warnings: Array.isArray(server.warnings) ? server.warnings : [],
230
259
  toolCount: Array.isArray(server.tools) ? server.tools.length : 0,
231
260
  tools: (server.tools || []).map((tool) => ({
232
261
  name: tool.name,
@@ -296,28 +325,98 @@ export function getMcpServerInstructionsMap(scopeId = DEFAULT_MCP_SCOPE_ID) {
296
325
  return out;
297
326
  }
298
327
 
328
+ // Close `current` (when registered) and connect `name` afresh with `cfg`.
329
+ // Callers run it inside the per-server reconnect lane. `genAtStart` is the
330
+ // shutdown generation captured BEFORE the caller queued/closed anything: a
331
+ // disconnectAll() landing during the close (or the connect) bumps it, and the
332
+ // replacement must then never register (no resurrection after shutdown).
333
+ async function replaceServerConnection(scopeId, name, current, cfg, genAtStart) {
334
+ const registryKey = mcpServerRegistryKey(scopeId, name);
335
+ if (current) {
336
+ await _closeServer(current);
337
+ if (servers.get(registryKey) === current) servers.delete(registryKey);
338
+ _invalidateMcpToolFieldMemo();
339
+ }
340
+ if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
341
+ throw new Error(`MCP server "${name}" connect aborted by shutdown`);
342
+ }
343
+ try {
344
+ // connectServer re-checks the same generation after loading the SDK and
345
+ // again right before registering, so a shutdown mid-handshake also aborts.
346
+ await connectServer(name, cfg, scopeId, genAtStart);
347
+ } catch (reason) {
348
+ const rawMessage = reason instanceof Error ? reason.message : String(reason);
349
+ throw new Error(scrubMcpConnectionMessage(rawMessage, cfg));
350
+ }
351
+ }
352
+
353
+ // Explicit single-server operations (connect/reconnect/disconnect) never join
354
+ // a failure-driven reconnect that is already in flight — its config may be
355
+ // stale — they queue behind it on the same lane instead, so at most one
356
+ // connect per server is ever running (no doubly registered transports).
357
+ async function runExclusiveServerOp(registryKey, task) {
358
+ for (let pending = reconnects.wait(registryKey); pending; pending = reconnects.wait(registryKey)) {
359
+ await pending.catch(() => {});
360
+ }
361
+ return reconnects.run(registryKey, task);
362
+ }
363
+
299
364
  async function reconnectMcpServer(scopeId, serverName, failedServer) {
300
365
  const registryKey = mcpServerRegistryKey(scopeId, serverName);
301
- return reconnects.run(registryKey, async () => {
366
+ // The generation the failed server was registered under, NOT the current
367
+ // one: when disconnectAll() itself caused the failure it has already bumped
368
+ // the generation (and still holds the entry while awaiting the close), so a
369
+ // freshly captured value would bless a post-shutdown reconnect.
370
+ const genAtStart = failedServer.generation ?? currentConnectAbortGeneration(scopeId);
371
+ await reconnects.run(registryKey, async () => {
302
372
  const current = servers.get(registryKey);
303
373
  // A peer already completed the replacement while this failed call was
304
374
  // unwinding. Reuse it immediately instead of closing a fresh transport.
305
- if (current && current !== failedServer) return current;
306
- if (current) {
307
- await _closeServer(current);
308
- if (servers.get(registryKey) === current) servers.delete(registryKey);
309
- }
310
- try {
311
- await connectServer(serverName, failedServer.cfg, scopeId);
312
- } catch (reason) {
313
- const rawMessage = reason instanceof Error ? reason.message : String(reason);
314
- throw new Error(scrubMcpConnectionMessage(rawMessage, failedServer.cfg));
375
+ if (current && current !== failedServer) return;
376
+ // Unregistered meanwhile (explicit disconnect / shutdown), shut down since
377
+ // it was registered, or being deliberately closed right now: a failed call
378
+ // must not resurrect a server someone deliberately removed.
379
+ if (!current || genAtStart !== currentConnectAbortGeneration(scopeId) || current.liveness?.closing) {
380
+ throw new Error(`MCP server "${serverName}" was disconnected`);
315
381
  }
316
- const replacement = servers.get(registryKey);
317
- if (!replacement) {
318
- throw new Error(`reconnect succeeded but server "${serverName}" entry is missing from registry`);
382
+ await replaceServerConnection(scopeId, serverName, current, failedServer.cfg, genAtStart);
383
+ });
384
+ const replacement = servers.get(registryKey);
385
+ if (!replacement) {
386
+ throw new Error(`reconnect succeeded but server "${serverName}" entry is missing from registry`);
387
+ }
388
+ return replacement;
389
+ }
390
+
391
+ /**
392
+ * Connect, or reconnect, ONE named MCP server in `scopeId` without touching
393
+ * any other server. A registered instance is closed first; `cfg` defaults to
394
+ * the registered config. `cfg.enabled === false` only disconnects. Resolves
395
+ * true when the server is connected afterwards, false when it was disabled;
396
+ * rejects (credential-scrubbed message) when the connect fails, leaving the
397
+ * server unregistered.
398
+ */
399
+ export async function reconnectSingleMcpServer(scopeId, name, cfg = undefined) {
400
+ const normalized = normalizeMcpScopeId(scopeId);
401
+ _knownMcpScopes.add(normalized);
402
+ const registryKey = mcpServerRegistryKey(normalized, name);
403
+ // Captured before queuing: a shutdown while this op waits or closes wins.
404
+ const genAtStart = currentConnectAbortGeneration(normalized);
405
+ return runExclusiveServerOp(registryKey, async () => {
406
+ const current = servers.get(registryKey);
407
+ const nextCfg = cfg ?? current?.cfg;
408
+ if (!nextCfg) throw new Error(`MCP server "${name}" has no config to connect with`);
409
+ if (nextCfg.enabled === false) {
410
+ if (current) {
411
+ await _closeServer(current);
412
+ if (servers.get(registryKey) === current) servers.delete(registryKey);
413
+ _invalidateMcpToolFieldMemo();
414
+ }
415
+ closeCallAdmission(normalized, name, `MCP ${name} disabled`);
416
+ return false;
319
417
  }
320
- return replacement;
418
+ await replaceServerConnection(normalized, name, current, nextCfg, genAtStart);
419
+ return true;
321
420
  });
322
421
  }
323
422
 
@@ -328,11 +427,12 @@ async function reconnectMcpServer(scopeId, serverName, failedServer) {
328
427
  export async function executeMcpTool(name, args, options = {}) {
329
428
  const match = name.match(MCP_TOOL_NAME_RE);
330
429
  if (!match) throw new Error(`Not an MCP tool name: ${name}`);
331
- const [, serverName, toolName] = match;
332
430
  const scopeId = normalizeMcpScopeId(options);
333
- const server = scopedServer(scopeId, serverName);
334
- if (!server) throw new Error(`MCP server "${serverName}" not connected`);
335
- const definition = (server.tools || []).find((tool) => tool.name === name);
431
+ const target = resolveMcpToolTarget(scopeId, name);
432
+ if (!target) throw new Error(`MCP server "${match[1]}" not connected`);
433
+ const { server, definition } = target;
434
+ const serverName = server.name;
435
+ const toolName = definition?.mcpToolName || match[2];
336
436
  const callSignal = options?.signal || null;
337
437
  const dispatch = (target) =>
338
438
  definition?.mcpOperation
@@ -366,6 +466,16 @@ export async function executeMcpTool(name, args, options = {}) {
366
466
  mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
367
467
  throw firstErr;
368
468
  }
469
+ const serverDead = server.liveness?.alive === false;
470
+ if (!serverDead && isMcpServerResponseError(firstErr)) {
471
+ // The server answered (JSON-RPC error): it is alive — nothing to
472
+ // reconnect, and the call already ran its course.
473
+ throw firstErr;
474
+ }
475
+ // Only a failure that proves the request never left this process
476
+ // may be replayed; anything else may already have run server-side.
477
+ const undelivered = isMcpUndeliveredError(firstErr);
478
+ const deadTail = serverDead ? server.stderrTail?.() : '';
369
479
  mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
370
480
  let retryServer;
371
481
  try {
@@ -382,7 +492,17 @@ export async function executeMcpTool(name, args, options = {}) {
382
492
  } catch (reconnectErr) {
383
493
  if (isMcpCallAbortError(reconnectErr) || callSignal?.aborted) throw reconnectErr;
384
494
  const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
385
- throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
495
+ throw new Error(
496
+ appendMcpStderrTail(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`, deadTail)
497
+ );
498
+ }
499
+ if (!undelivered) {
500
+ throw new Error(
501
+ appendMcpStderrTail(
502
+ `Tool call failed: ${firstMsg}; MCP server "${serverName}" was reconnected for the next call, but this call was not replayed because the request may already have reached the server`,
503
+ deadTail
504
+ )
505
+ );
386
506
  }
387
507
  // A reconnect that outlived the caller's abort must not dispatch the
388
508
  // retry: that would run the side effect after cancellation.
@@ -410,71 +530,120 @@ export async function executeMcpTool(name, args, options = {}) {
410
530
  );
411
531
  }
412
532
 
413
- async function _callMcpFeatureWithTimeout(server, operation, args, signal = null) {
414
- const requestOptions = signal ? { signal } : undefined;
415
- const cursor = typeof args?.cursor === 'string' && args.cursor ? { cursor: args.cursor } : {};
416
- let request;
417
- if (operation === 'list-resources') {
418
- request = server.client.listResources(cursor, requestOptions);
419
- } else if (operation === 'list-resource-templates') {
420
- request = server.client.listResourceTemplates(cursor, requestOptions);
421
- } else if (operation === 'read-resource') {
422
- request = server.client.readResource({ uri: String(args?.uri || '') }, requestOptions);
423
- } else if (operation === 'list-prompts') {
424
- request = server.client.listPrompts(cursor, requestOptions);
425
- } else if (operation === 'get-prompt') {
426
- request = server.client.getPrompt(
533
+ const MCP_FEATURE_REQUESTS = {
534
+ 'list-resources': (client, args, options) => client.listResources(mcpCursorParams(args), options),
535
+ 'list-resource-templates': (client, args, options) => client.listResourceTemplates(mcpCursorParams(args), options),
536
+ 'read-resource': (client, args, options) => client.readResource({ uri: String(args?.uri || '') }, options),
537
+ 'list-prompts': (client, args, options) => client.listPrompts(mcpCursorParams(args), options),
538
+ 'get-prompt': (client, args, options) =>
539
+ client.getPrompt(
427
540
  {
428
541
  name: String(args?.name || ''),
429
542
  arguments:
430
543
  args?.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments) ? args.arguments : {},
431
544
  },
432
- requestOptions
433
- );
434
- } else {
435
- throw new Error(`Unsupported MCP feature operation: ${operation}`);
545
+ options
546
+ ),
547
+ };
548
+
549
+ function mcpCursorParams(args) {
550
+ return typeof args?.cursor === 'string' && args.cursor ? { cursor: args.cursor } : {};
551
+ }
552
+
553
+ async function _callMcpFeatureWithTimeout(server, operation, args, signal = null) {
554
+ const issue = MCP_FEATURE_REQUESTS[operation];
555
+ if (!issue) throw new Error(`Unsupported MCP feature operation: ${operation}`);
556
+ const result = await runMcpRequest(server, signal, (options) => issue(server.client, args, options), {
557
+ abortMessage: `MCP feature call aborted (server="${server?.name}", operation="${operation}")`,
558
+ timeoutMessage: (ms) =>
559
+ `MCP feature call timed out after ${ms}ms without progress (server="${server.name}", operation="${operation}")`,
560
+ errorFields: { serverName: server.name, operation },
561
+ });
562
+ return {
563
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
564
+ isError: false,
565
+ };
566
+ }
567
+
568
+ // Our per-call deadline. It is reset by every progress notification, but never
569
+ // beyond maxTotalTimeoutMs from the start. On expiry the call rejects with
570
+ // EMCPTOOLTIMEOUT and `cancelRequest` cancels ONLY this request (the SDK then
571
+ // sends notifications/cancelled); the server and its other in-flight calls
572
+ // are untouched. The caller clears the timer once its own race settles.
573
+ function mcpCallTimeout({ callTimeoutMs, maxTotalTimeoutMs }, message, errorFields, cancelRequest) {
574
+ const startedAt = Date.now();
575
+ let timer = null;
576
+ let rejectTimeout;
577
+ const promise = new Promise((_, reject) => {
578
+ rejectTimeout = reject;
579
+ });
580
+ const fire = () => {
581
+ timer = null;
582
+ const error = Object.assign(new Error(message), { code: 'EMCPTOOLTIMEOUT', ...errorFields });
583
+ rejectTimeout(error);
584
+ cancelRequest(error);
585
+ };
586
+ const arm = (ms) => {
587
+ if (timer) clearTimeout(timer);
588
+ timer = setTimeout(fire, ms);
589
+ if (timer.unref) timer.unref();
590
+ };
591
+ arm(callTimeoutMs);
592
+ return {
593
+ promise,
594
+ clear: () => {
595
+ if (timer) clearTimeout(timer);
596
+ timer = null;
597
+ },
598
+ progress: () => {
599
+ if (!timer) return;
600
+ const remainingTotal = maxTotalTimeoutMs ? maxTotalTimeoutMs - (Date.now() - startedAt) : Infinity;
601
+ if (remainingTotal > 0) arm(Math.min(callTimeoutMs, remainingTotal));
602
+ },
603
+ };
604
+ }
605
+
606
+ /**
607
+ * Issue one SDK request under the per-call limits. The request gets its OWN
608
+ * abort signal (linked to the caller's), our timeout as the SDK `timeout`
609
+ * (or the largest timer when disabled, so the SDK's 60s default never
610
+ * applies), and progress handling: each progress report resets both the SDK
611
+ * timeout and ours, capped by `maxTotalTimeout`.
612
+ */
613
+ async function runMcpRequest(server, signal, issue, { abortMessage, timeoutMessage, errorFields }) {
614
+ const timeouts = resolveMcpRequestTimeouts(server?.cfg);
615
+ const controller = new AbortController();
616
+ const forwardAbort = () => controller.abort(signal.reason);
617
+ if (signal) {
618
+ allowConcurrentAbortListeners(signal);
619
+ if (signal.aborted) forwardAbort();
620
+ else signal.addEventListener('abort', forwardAbort, { once: true });
436
621
  }
437
- const timeoutMs = resolveMcpCallTimeoutMs(server?.cfg);
438
- const abortMessage = `MCP feature call aborted (server="${server?.name}", operation="${operation}")`;
439
622
  const timeout =
440
- timeoutMs > 0
623
+ timeouts.callTimeoutMs > 0
441
624
  ? mcpCallTimeout(
442
- server,
443
- timeoutMs,
444
- `MCP feature call timed out after ${timeoutMs}ms (server="${server.name}", operation="${operation}")`
625
+ timeouts,
626
+ timeoutMessage(timeouts.callTimeoutMs),
627
+ { ...errorFields, timeoutMs: timeouts.callTimeoutMs },
628
+ (error) => controller.abort(error)
445
629
  )
446
630
  : null;
631
+ const requestOptions = {
632
+ signal: controller.signal,
633
+ timeout: timeouts.sdkTimeoutMs,
634
+ resetTimeoutOnProgress: true,
635
+ onprogress: () => timeout?.progress(),
636
+ ...(timeouts.maxTotalTimeoutMs ? { maxTotalTimeout: timeouts.maxTotalTimeoutMs } : {}),
637
+ };
447
638
  try {
448
- const result = await raceMcpAbort(
449
- timeout ? Promise.race([request, timeout.promise]) : request,
450
- signal,
451
- abortMessage
452
- );
453
- return {
454
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
455
- isError: false,
456
- };
639
+ const request = issue(requestOptions);
640
+ return await raceMcpAbort(timeout ? Promise.race([request, timeout.promise]) : request, signal, abortMessage);
457
641
  } finally {
458
642
  timeout?.clear();
643
+ signal?.removeEventListener?.('abort', forwardAbort);
459
644
  }
460
645
  }
461
646
 
462
- // Server-side per-call deadline. On expiry the server goes through the full
463
- // tree-shutdown path so a timed-out stdio server never orphans grandchildren
464
- // (fire-and-forget), and the call rejects with EMCPTOOLTIMEOUT. The caller
465
- // clears the timer once its own (abort-aware) race settles.
466
- function mcpCallTimeout(server, timeoutMs, message, errorFields = {}) {
467
- let timer;
468
- const promise = new Promise((_, reject) => {
469
- timer = setTimeout(() => {
470
- _closeServer(server).catch(() => {});
471
- reject(Object.assign(new Error(message), { code: 'EMCPTOOLTIMEOUT', ...errorFields }));
472
- }, timeoutMs);
473
- if (timer.unref) timer.unref();
474
- });
475
- return { promise, clear: () => clearTimeout(timer) };
476
- }
477
-
478
647
  function mcpResourceFilename(uri) {
479
648
  const tail = String(uri || '')
480
649
  .split(/[?#]/)[0]
@@ -506,14 +675,21 @@ function mcpCanonicalBase64(data, maxBytes) {
506
675
  const mcpMiB = (bytes) => `${bytes / 1024 / 1024} MiB`;
507
676
 
508
677
  function mcpImagePart(data, rawMime) {
509
- const mimeType = String(rawMime || 'image/png').split(';')[0].trim().toLowerCase() || 'image/png';
678
+ const mimeType =
679
+ String(rawMime || 'image/png')
680
+ .split(';')[0]
681
+ .trim()
682
+ .toLowerCase() || 'image/png';
510
683
  if (!SUPPORTED_IMAGE_MIME_TYPES.has(mimeType)) {
511
684
  return { type: 'text', text: `[MCP image omitted: ${mimeType} is not a supported image type]` };
512
685
  }
513
686
  const canon = mcpCanonicalBase64(data, MAX_IMAGE_BYTES);
514
687
  if (!canon) return { type: 'text', text: '[MCP image omitted: empty image data]' };
515
688
  if (canon.tooLarge) {
516
- return { type: 'text', text: `[MCP image omitted: ${mimeType} image exceeds the ${mcpMiB(MAX_IMAGE_BYTES)} limit]` };
689
+ return {
690
+ type: 'text',
691
+ text: `[MCP image omitted: ${mimeType} image exceeds the ${mcpMiB(MAX_IMAGE_BYTES)} limit]`,
692
+ };
517
693
  }
518
694
  return { type: 'image', data: canon.data, mimeType };
519
695
  }
@@ -584,10 +760,19 @@ function mcpAudioPart(data, rawMime) {
584
760
  const canon = typeof data === 'string' && data ? mcpCanonicalBase64(data, MAX_ATTACHMENT_BLOB_BYTES) : null;
585
761
  if (!canon) return { type: 'text', text: `[MCP audio content: ${mimeType}]` };
586
762
  if (canon.tooLarge) {
587
- return { type: 'text', text: `[MCP audio omitted: ${mimeType} exceeds the ${mcpMiB(MAX_ATTACHMENT_BLOB_BYTES)} limit]` };
763
+ return {
764
+ type: 'text',
765
+ text: `[MCP audio omitted: ${mimeType} exceeds the ${mcpMiB(MAX_ATTACHMENT_BLOB_BYTES)} limit]`,
766
+ };
588
767
  }
589
768
  const subtype = mimeType.split(';')[0].split('/')[1] || '';
590
- const path = spillMcpBinary(canon.buffer, subtype.replace(/[^a-z0-9]/gi, '').slice(0, 8).toLowerCase() || 'bin');
769
+ const path = spillMcpBinary(
770
+ canon.buffer,
771
+ subtype
772
+ .replace(/[^a-z0-9]/gi, '')
773
+ .slice(0, 8)
774
+ .toLowerCase() || 'bin'
775
+ );
591
776
  return {
592
777
  type: 'text',
593
778
  text: path
@@ -648,8 +833,32 @@ export async function normalizeMcpToolResult(result) {
648
833
  return parts ? { content: parts } : text;
649
834
  }
650
835
 
836
+ // JSON-RPC codes the SDK uses for client-side failures (McpError).
837
+ const MCP_CONNECTION_CLOSED_CODE = -32000;
838
+ const MCP_REQUEST_TIMEOUT_CODE = -32001;
839
+
840
+ // Our own deadline, or the SDK's (McpError RequestTimeout, incl. the
841
+ // progress maxTotalTimeout).
651
842
  function isMcpToolCallTimeoutError(err) {
652
- return err?.code === 'EMCPTOOLTIMEOUT';
843
+ return err?.code === 'EMCPTOOLTIMEOUT' || err?.code === MCP_REQUEST_TIMEOUT_CODE;
844
+ }
845
+
846
+ // The server answered with a JSON-RPC error: proof it is alive.
847
+ function isMcpServerResponseError(err) {
848
+ return (
849
+ typeof err?.code === 'number' && err.code !== MCP_CONNECTION_CLOSED_CODE && err.code !== MCP_REQUEST_TIMEOUT_CODE
850
+ );
851
+ }
852
+
853
+ // Connection-level failures that prove the request was never delivered: the
854
+ // SDK/transport had no live connection to send on, or the socket was refused
855
+ // before anything was written. ECONNRESET etc. are excluded — the request may
856
+ // have been read before the reset.
857
+ const MCP_UNDELIVERED_CODES = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH', 'ENETUNREACH']);
858
+ function isMcpUndeliveredError(err) {
859
+ if (!err) return false;
860
+ if (/^Not connected$/i.test(String(err.message || '').trim())) return true;
861
+ return MCP_UNDELIVERED_CODES.has(String(err.code || err.cause?.code || ''));
653
862
  }
654
863
 
655
864
  // Caller-driven cancellation (hook timeout, aborted turn, closed pane).
@@ -714,38 +923,22 @@ export function raceMcpAbort(promise, signal, message) {
714
923
  }
715
924
 
716
925
  async function _callToolWithTimeout(server, toolName, args, signal = null) {
717
- const timeoutMs = resolveMcpCallTimeoutMs(server?.cfg);
718
- // The signal goes INTO the SDK request: an aborted caller cancels the live
719
- // JSON-RPC request (notifications/cancelled) instead of merely walking away
720
- // from it. The race below additionally frees the admission slot at once.
721
- const requestOptions = signal ? { signal } : undefined;
722
- const abortMessage = `MCP tool call aborted (server="${server?.name}", tool="${toolName}")`;
723
- if (!(timeoutMs > 0)) {
724
- // Server timeout disabled: the caller's signal is then the ONLY bound.
725
- return raceMcpAbort(
726
- server.client.callTool({ name: toolName, arguments: args }, undefined, requestOptions),
727
- signal,
728
- abortMessage
729
- );
730
- }
731
- const timeout = mcpCallTimeout(
926
+ // The request's signal goes INTO the SDK: an aborted caller (or our own
927
+ // timeout) cancels the live JSON-RPC request (notifications/cancelled)
928
+ // instead of merely walking away from it. The abort race additionally frees
929
+ // the admission slot at once. With the timeout disabled the caller's signal
930
+ // is the ONLY bound.
931
+ return runMcpRequest(
732
932
  server,
733
- timeoutMs,
734
- `MCP tool call timed out after ${timeoutMs}ms (server="${server.name}", tool="${toolName}")`,
735
- { serverName: server.name, toolName, timeoutMs }
933
+ signal,
934
+ (options) => server.client.callTool({ name: toolName, arguments: args }, undefined, options),
935
+ {
936
+ abortMessage: `MCP tool call aborted (server="${server?.name}", tool="${toolName}")`,
937
+ timeoutMessage: (ms) =>
938
+ `MCP tool call timed out after ${ms}ms without progress (server="${server.name}", tool="${toolName}")`,
939
+ errorFields: { serverName: server.name, toolName },
940
+ }
736
941
  );
737
- try {
738
- return await raceMcpAbort(
739
- Promise.race([
740
- server.client.callTool({ name: toolName, arguments: args }, undefined, requestOptions),
741
- timeout.promise,
742
- ]),
743
- signal,
744
- abortMessage
745
- );
746
- } finally {
747
- timeout.clear();
748
- }
749
942
  }
750
943
 
751
944
  function countTextLines(text) {
@@ -848,12 +1041,7 @@ export function isMcpTool(name) {
848
1041
  }
849
1042
  /** True when the prefixed name exists on a connected MCP server. */
850
1043
  export function isRegisteredMcpTool(name, scopeId = DEFAULT_MCP_SCOPE_ID) {
851
- const match = name.match(MCP_TOOL_NAME_RE);
852
- if (!match) return false;
853
- const [, serverName] = match;
854
- const server = scopedServer(scopeId, serverName);
855
- if (!server || !Array.isArray(server.tools)) return false;
856
- return server.tools.some((t) => t?.name === name);
1044
+ return Boolean(resolveMcpToolTarget(scopeId, name)?.definition);
857
1045
  }
858
1046
  /**
859
1047
  * Check whether the inputSchema for an MCP tool declares the given top-level
@@ -866,9 +1054,7 @@ export function mcpToolHasField(name, field, scopeId = DEFAULT_MCP_SCOPE_ID) {
866
1054
  const memoKey = `${normalizedScopeId}|${name}|${field}`;
867
1055
  const memoized = _mcpToolFieldMemo.get(memoKey);
868
1056
  if (memoized !== undefined) return memoized;
869
- const match = name.match(MCP_TOOL_NAME_RE);
870
- const server = match ? scopedServer(normalizedScopeId, match[1]) : null;
871
- const tool = server ? server.tools.find((t) => t.name === name) : null;
1057
+ const tool = resolveMcpToolTarget(normalizedScopeId, name)?.definition;
872
1058
  const props = tool?.inputSchema?.properties;
873
1059
  const result = Boolean(props && Object.hasOwn(props, field));
874
1060
  _mcpToolFieldMemo.set(memoKey, result);
@@ -932,17 +1118,19 @@ export async function disconnectAll(options = undefined) {
932
1118
  export async function disconnectMcpServer(name, options = {}) {
933
1119
  const scopeId = normalizeMcpScopeId(options);
934
1120
  const registryKey = mcpServerRegistryKey(scopeId, name);
935
- const server = servers.get(registryKey);
936
- if (!server) return false;
937
- try {
938
- await _closeServer(server);
939
- } catch {
940
- /* ignore */
941
- }
942
- servers.delete(registryKey);
943
- closeCallAdmission(scopeId, name, `MCP ${name} disconnected`);
944
- _invalidateMcpToolFieldMemo();
945
- return true;
1121
+ return runExclusiveServerOp(registryKey, async () => {
1122
+ const server = servers.get(registryKey);
1123
+ if (!server) return false;
1124
+ try {
1125
+ await _closeServer(server);
1126
+ } catch {
1127
+ /* ignore */
1128
+ }
1129
+ if (servers.get(registryKey) === server) servers.delete(registryKey);
1130
+ closeCallAdmission(scopeId, name, `MCP ${name} disconnected`);
1131
+ _invalidateMcpToolFieldMemo();
1132
+ return true;
1133
+ });
946
1134
  }
947
1135
  /**
948
1136
  * Close a single server: for stdio transports first shut down the full child
@@ -952,6 +1140,8 @@ export async function disconnectMcpServer(name, options = {}) {
952
1140
  * only kills the direct child and discards the pid we need to walk the tree.
953
1141
  */
954
1142
  async function _closeServer(server) {
1143
+ // A deliberate close is not a crash: suppress the unexpected-close report.
1144
+ if (server?.liveness) server.liveness.closing = true;
955
1145
  const transport = server?.transport;
956
1146
  // Live stdio transports expose the spawned ChildProcess on _process.
957
1147
  if (transport?._process) {
@@ -985,8 +1175,15 @@ async function connectServer(
985
1175
  // abort before creating a transport/child at all.
986
1176
  throw new Error(`MCP server "${name}" connect aborted by shutdown`);
987
1177
  }
988
- const client = createMcpClient(sdk, name, cfg);
989
- const { transport, autoDetectAdvert } = createMcpTransport({ name, cfg, sdk, log: mcpLog });
1178
+ const client = createMcpClient(sdk, name, cfg, scopeId);
1179
+ const { transport, autoDetectAdvert, stderrTail, warnings } = createMcpTransport({
1180
+ name,
1181
+ cfg,
1182
+ sdk,
1183
+ log: mcpLog,
1184
+ strictEnv: cfg?._mixdogStrictEnv === true,
1185
+ });
1186
+ const liveness = trackMcpLiveness(client, name, cfg, stderrTail);
990
1187
  const pending = { scopeId, name, client, transport };
991
1188
  _pendingConnects.add(pending);
992
1189
  try {
@@ -996,11 +1193,12 @@ async function connectServer(
996
1193
  client,
997
1194
  transport,
998
1195
  autoDetectAdvert,
999
- closeServer: _closeServer,
1196
+ closeServer: (target) => _closeServer({ ...target, liveness }),
1197
+ stderrTail,
1000
1198
  });
1001
1199
  if (!toolsResult || !Array.isArray(toolsResult.tools)) {
1002
1200
  try {
1003
- await _closeServer({ client, transport });
1201
+ await _closeServer({ client, transport, liveness });
1004
1202
  } catch {
1005
1203
  /* ignore */
1006
1204
  }
@@ -1009,13 +1207,13 @@ async function connectServer(
1009
1207
  if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
1010
1208
  // disconnectAll() ran mid-handshake: never register — tear down.
1011
1209
  try {
1012
- await _closeServer({ client, transport });
1210
+ await _closeServer({ client, transport, liveness });
1013
1211
  } catch {
1014
1212
  /* ignore */
1015
1213
  }
1016
1214
  throw new Error(`MCP server "${name}" connect aborted by shutdown`);
1017
1215
  }
1018
- const tools = mcpServerTools(name, capabilities, toolsResult.tools);
1216
+ const tools = mcpServerTools(name, capabilities, toolsResult.tools, cfg);
1019
1217
  const registryKey = mcpServerRegistryKey(scopeId, name);
1020
1218
  servers.set(registryKey, {
1021
1219
  scopeId,
@@ -1027,6 +1225,9 @@ async function connectServer(
1027
1225
  cfg,
1028
1226
  instructions,
1029
1227
  capabilities,
1228
+ liveness,
1229
+ stderrTail,
1230
+ warnings: warnings || [],
1030
1231
  generation: genAtStart,
1031
1232
  });
1032
1233
  if (capabilities?.tools?.listChanged && sdk.ToolListChangedNotificationSchema) {
@@ -1041,16 +1242,52 @@ async function connectServer(
1041
1242
  }
1042
1243
  }
1043
1244
 
1044
- /** The SDK client for one server, advertising the project root when known. */
1045
- function createMcpClient(sdk, name, cfg) {
1046
- const projectRoot = String(cfg?._mixdogProjectRoot || cfg?.cwd || '').trim();
1245
+ /**
1246
+ * Liveness of one server connection. The SDK calls client.onclose whenever
1247
+ * the transport closes (child exit, socket drop, our own close) — the server
1248
+ * is then dead and getMcpServerStatus reports it disconnected. onerror only
1249
+ * records the last error (parse/send hiccups do not mean the server died).
1250
+ * Handlers already installed on the client are chained, not replaced.
1251
+ */
1252
+ function trackMcpLiveness(client, name, cfg, stderrTail) {
1253
+ const liveness = { alive: true, closing: false, closedAt: 0, lastError: '' };
1254
+ const priorClose = client.onclose;
1255
+ const priorError = client.onerror;
1256
+ client.onclose = (...args) => {
1257
+ const unexpected = liveness.alive && !liveness.closing;
1258
+ liveness.alive = false;
1259
+ liveness.closedAt = Date.now();
1260
+ if (unexpected) {
1261
+ mcpLog(`${appendMcpStderrTail(`[mcp-client] MCP server "${name}" closed unexpectedly`, stderrTail())}\n`);
1262
+ }
1263
+ if (typeof priorClose === 'function') priorClose.apply(client, args);
1264
+ };
1265
+ client.onerror = (error, ...rest) => {
1266
+ liveness.lastError = scrubMcpConnectionMessage(error instanceof Error ? error.message : String(error), cfg);
1267
+ if (typeof priorError === 'function') priorError.call(client, error, ...rest);
1268
+ };
1269
+ return liveness;
1270
+ }
1271
+
1272
+ /** The SDK client for one server, advertising the project root when known.
1273
+ * A config marked `_mixdogRootsFollowScope` advertises its scope's current
1274
+ * root (setMcpScopeRoot) and is told when it changes. */
1275
+ function createMcpClient(sdk, name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID) {
1276
+ const followScope = cfg?._mixdogRootsFollowScope === true;
1277
+ const fixedRoot = String(cfg?._mixdogProjectRoot || cfg?.cwd || '').trim();
1278
+ const currentRoot = () => (followScope ? _scopeRoots.get(normalizeMcpScopeId(scopeId)) || fixedRoot : fixedRoot);
1047
1279
  const client = new sdk.Client(
1048
1280
  { name: `mixdog-agent/${name}`, version: '1.0.0' },
1049
- { capabilities: projectRoot ? { roots: { listChanged: false } } : {} }
1281
+ {
1282
+ capabilities: followScope || fixedRoot ? { roots: { listChanged: followScope } } : {},
1283
+ jsonSchemaValidator: createLazyOutputValidator(sdk.AjvJsonSchemaValidator),
1284
+ }
1050
1285
  );
1051
- if (projectRoot && sdk.ListRootsRequestSchema) {
1286
+ if ((followScope || fixedRoot) && sdk.ListRootsRequestSchema) {
1052
1287
  client.setRequestHandler(sdk.ListRootsRequestSchema, async () => {
1053
- const rootPath = resolve(projectRoot);
1288
+ const root = currentRoot();
1289
+ if (!root) return { roots: [] };
1290
+ const rootPath = resolve(root);
1054
1291
  return {
1055
1292
  roots: [
1056
1293
  {
@@ -1064,23 +1301,85 @@ function createMcpClient(sdk, name, cfg) {
1064
1301
  return client;
1065
1302
  }
1066
1303
 
1067
- /** Protocol tools under the `mcp__<server>__` prefix plus the feature tools. */
1068
- function mcpServerTools(name, capabilities, rawTools) {
1069
- const protocolTools = (rawTools || []).map((t) => ({
1070
- name: `mcp__${name}__${t.name}`,
1071
- description: t.description || '',
1072
- inputSchema: t.inputSchema || { type: 'object', properties: {} },
1073
- ...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
1074
- }));
1075
- return [...protocolTools, ...mcpFeatureTools(name, capabilities, protocolTools)];
1304
+ /**
1305
+ * Set the root advertised by `scopeId`'s scope-following servers (plugin
1306
+ * servers, whose roots track the session cwd) and notify the connected ones
1307
+ * when it changed. No reconnect: the connection and its tools stay as they are.
1308
+ */
1309
+ export function setMcpScopeRoot(scopeId, root) {
1310
+ const normalized = normalizeMcpScopeId(scopeId);
1311
+ const next = String(root || '').trim();
1312
+ if ((_scopeRoots.get(normalized) || '') === next) return;
1313
+ if (next) _scopeRoots.set(normalized, next);
1314
+ else _scopeRoots.delete(normalized);
1315
+ for (const [, server] of scopedServerEntries(normalized)) {
1316
+ if (server.cfg?._mixdogRootsFollowScope !== true) continue;
1317
+ Promise.resolve()
1318
+ .then(() => server.client?.sendRootsListChanged?.())
1319
+ .catch(() => {});
1320
+ }
1076
1321
  }
1077
1322
 
1078
- async function refreshServerTools(registryKey, client, name, cfg, capabilities) {
1323
+ /** The connected server and tool definition a prefixed tool name refers to.
1324
+ * The name's server part finds it directly; a name shortened to fit provider
1325
+ * limits is found by scanning this scope's servers. A known server without
1326
+ * the tool still resolves (definition null) so the call reaches the server. */
1327
+ function resolveMcpToolTarget(scopeId, name) {
1328
+ const match = MCP_TOOL_NAME_RE.exec(String(name || ''));
1329
+ if (!match) return null;
1330
+ const direct = scopedServer(scopeId, match[1]);
1331
+ const directTool = direct?.tools?.find((tool) => tool.name === name);
1332
+ if (directTool) return { server: direct, definition: directTool };
1333
+ for (const [, server] of scopedServerEntries(scopeId)) {
1334
+ const definition = server.tools?.find((tool) => tool.name === name);
1335
+ if (definition) return { server, definition };
1336
+ }
1337
+ return direct ? { server: direct, definition: null } : null;
1338
+ }
1339
+
1340
+ /** Protocol tools under the `mcp__<server>__` prefix plus the feature tools.
1341
+ * Names that would exceed provider limits are shortened deterministically
1342
+ * (`providerSafeMcpToolName`); a tool whose name was shortened carries the
1343
+ * server's own tool name as `mcpToolName` and its server as `mcpServerName`.
1344
+ * A plugin-declared server's tools carry its owner as `mcpPluginId`, so
1345
+ * scope filtering never has to infer ownership from the server name. */
1346
+ function mcpServerTools(name, capabilities, rawTools, cfg) {
1347
+ const protocolTools = (rawTools || []).map((t) => {
1348
+ const safeName = providerSafeMcpToolName(name, t.name);
1349
+ return {
1350
+ name: safeName,
1351
+ ...(safeName === `mcp__${name}__${t.name}` ? {} : { mcpToolName: t.name, mcpServerName: name }),
1352
+ description: t.description || '',
1353
+ inputSchema: t.inputSchema || { type: 'object', properties: {} },
1354
+ ...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
1355
+ };
1356
+ });
1357
+ const tools = [...protocolTools, ...mcpFeatureTools(name, capabilities, protocolTools)];
1358
+ const pluginId = String(cfg?._mixdogPlugin?.id || '').trim();
1359
+ return pluginId ? tools.map((tool) => ({ ...tool, mcpPluginId: pluginId })) : tools;
1360
+ }
1361
+
1362
+ // tools/list_changed refreshes run one at a time per server connection, so an
1363
+ // older listing can never land after a newer one.
1364
+ function refreshServerTools(registryKey, client, name, cfg, capabilities) {
1079
1365
  const live = servers.get(registryKey);
1080
- if (!live || live.client !== client) return;
1366
+ if (!live || live.client !== client) return Promise.resolve();
1367
+ live.refreshTail = (live.refreshTail || Promise.resolve()).then(() =>
1368
+ runToolRefresh(registryKey, live, name, cfg, capabilities)
1369
+ );
1370
+ return live.refreshTail;
1371
+ }
1372
+
1373
+ async function runToolRefresh(registryKey, live, name, cfg, capabilities) {
1374
+ if (servers.get(registryKey) !== live) return;
1081
1375
  try {
1082
- const refreshed = await client.listTools();
1083
- live.tools = mcpServerTools(name, capabilities, refreshed.tools);
1376
+ const { sdkTimeoutMs } = resolveMcpRequestTimeouts(cfg);
1377
+ const refreshed = await listAllMcpTools(live.client, { timeout: sdkTimeoutMs });
1378
+ if (!refreshed || !Array.isArray(refreshed.tools)) {
1379
+ throw new Error('ListTools returned invalid shape: missing or non-array tools field');
1380
+ }
1381
+ if (servers.get(registryKey) !== live) return;
1382
+ live.tools = mcpServerTools(name, capabilities, refreshed.tools, cfg);
1084
1383
  _invalidateMcpToolFieldMemo();
1085
1384
  } catch (error) {
1086
1385
  mcpLog(
@@ -1093,22 +1392,33 @@ async function refreshServerTools(registryKey, client, name, cfg, capabilities)
1093
1392
  export function _registerMcpServerForTest(scopeId, name, rawTools = [], options = {}) {
1094
1393
  const normalizedScopeId = normalizeMcpScopeId(scopeId);
1095
1394
  const registryKey = mcpServerRegistryKey(normalizedScopeId, name);
1096
- const tools = rawTools.map((tool) => ({
1097
- ...tool,
1098
- name: String(tool?.name || '').startsWith('mcp__')
1099
- ? String(tool.name)
1100
- : `mcp__${name}__${String(tool?.name || '')}`,
1101
- inputSchema: tool?.inputSchema || { type: 'object', properties: {} },
1102
- }));
1395
+ const tools = rawTools.map((tool) => {
1396
+ const prefixed = String(tool?.name || '').startsWith('mcp__');
1397
+ const leaf = String(tool?.name || '');
1398
+ const safeName = prefixed ? leaf : providerSafeMcpToolName(name, leaf);
1399
+ const shortened = !prefixed && safeName !== `mcp__${name}__${leaf}`;
1400
+ return {
1401
+ ...tool,
1402
+ name: safeName,
1403
+ ...(shortened ? { mcpToolName: leaf, mcpServerName: name } : {}),
1404
+ inputSchema: tool?.inputSchema || { type: 'object', properties: {} },
1405
+ };
1406
+ });
1103
1407
  const capabilities = options.capabilities || {};
1408
+ const pluginId = String(options.cfg?._mixdogPlugin?.id || '').trim();
1104
1409
  const server = {
1105
1410
  scopeId: normalizedScopeId,
1106
1411
  registryKey,
1107
1412
  name,
1108
- tools: [...tools, ...mcpFeatureTools(name, capabilities, tools)],
1413
+ tools: [...tools, ...mcpFeatureTools(name, capabilities, tools)].map((tool) =>
1414
+ pluginId ? { ...tool, mcpPluginId: pluginId } : tool
1415
+ ),
1416
+ warnings: options.warnings || [],
1109
1417
  cfg: options.cfg || {},
1110
1418
  instructions: options.instructions || '',
1111
1419
  capabilities,
1420
+ liveness: { alive: true, closing: false, closedAt: 0, lastError: '' },
1421
+ stderrTail: typeof options.stderrTail === 'function' ? options.stderrTail : () => '',
1112
1422
  transport: null,
1113
1423
  client: {
1114
1424
  callTool: