mixdog 1.2.0 → 1.2.2

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 (386) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/scripts/gate-local.mjs +19 -3
  4. package/scripts/model-catalog-audit.mjs +1 -15
  5. package/src/agents/reviewer/AGENT.md +3 -3
  6. package/src/agents/reviewer/agent.json +1 -1
  7. package/src/defaults/shipped-history.json +5 -0
  8. package/src/defaults/skills/pptx/SKILL.md +6 -6
  9. package/src/defaults/skills/pptx/references/review.md +6 -4
  10. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  11. package/src/defaults/skills/setup/references/actions.md +13 -3
  12. package/src/defaults/skills/visualize/SKILL.md +12 -9
  13. package/src/repl/slash-commands.mjs +16 -1
  14. package/src/rules/shared/60-verification.md +3 -1
  15. package/src/rules/shared/70-delivery.md +4 -2
  16. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  17. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  18. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  19. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  20. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  21. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  22. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  23. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +122 -15
  24. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +54 -11
  25. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +104 -9
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +358 -140
  27. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  29. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  30. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  34. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  36. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  37. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +294 -44
  38. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  40. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  42. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  43. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  44. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  45. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  46. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  49. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  50. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  52. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  53. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  54. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  55. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  56. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +53 -18
  57. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  58. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  59. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  60. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  61. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  62. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  63. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  64. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  65. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  66. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +447 -0
  67. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  68. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  70. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  71. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  72. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  73. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  74. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  75. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +86 -19
  76. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  78. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  79. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  80. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  81. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  82. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  83. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  84. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  85. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  86. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  87. package/src/runtime/agent/orchestrator/providers/gemini.mjs +97 -51
  88. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  89. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  90. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  91. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  92. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  94. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  95. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  96. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  97. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  98. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  99. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +5 -3
  100. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  101. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  103. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  109. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  110. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  111. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  112. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  113. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +6 -3
  114. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  115. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +26 -6
  116. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  117. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  119. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  120. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  121. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  123. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  124. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  125. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  126. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  127. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  128. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  129. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  130. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  131. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  132. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  133. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  134. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  135. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +35 -24
  136. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  137. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  138. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  139. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  140. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  141. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  142. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  143. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  144. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  145. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  146. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  147. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  148. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  149. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  150. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +268 -181
  151. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  152. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  153. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  154. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  155. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +270 -0
  156. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  157. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  158. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  159. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  160. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  161. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  162. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  163. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  164. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  165. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  166. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  167. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  168. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  169. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -4
  170. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  171. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  172. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  173. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  174. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +42 -5
  175. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +14 -3
  176. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  177. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +50 -23
  178. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  179. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +22 -14
  180. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  181. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  182. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  183. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  184. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  185. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  186. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  187. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  188. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  189. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  190. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +819 -0
  191. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +12 -0
  192. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +22 -12
  193. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  194. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  195. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  196. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  197. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +41 -3
  198. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  199. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  200. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  201. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  202. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  203. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  204. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  205. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  206. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  207. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  208. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  209. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  210. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  211. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  212. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  213. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  214. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  215. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  216. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  217. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  218. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  219. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  220. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  221. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  222. package/src/runtime/attachments/store.d.mts +3 -0
  223. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  224. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  225. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  226. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  227. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  228. package/src/runtime/memory/lib/memory.mjs +1 -1
  229. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  230. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  231. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  232. package/src/runtime/office/README.md +1 -1
  233. package/src/runtime/office/shared/values.mjs +0 -5
  234. package/src/runtime/shared/atomic-file.mjs +25 -4
  235. package/src/runtime/shared/background-tasks.mjs +92 -14
  236. package/src/runtime/shared/err-text.mjs +25 -0
  237. package/src/runtime/shared/error-presentation.mjs +159 -19
  238. package/src/runtime/shared/extension-scopes.mjs +5 -3
  239. package/src/runtime/shared/file-lock.mjs +37 -11
  240. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  241. package/src/runtime/shared/llm/anthropic-effort.mjs +134 -131
  242. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  243. package/src/runtime/shared/llm/model-catalog.mjs +53 -52
  244. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  245. package/src/runtime/shared/llm/provider-catalog-registry.mjs +17 -0
  246. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  247. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  248. package/src/runtime/shared/model-display.mjs +13 -0
  249. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  250. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  251. package/src/runtime/shared/pristine-execution.mjs +28 -4
  252. package/src/runtime/shared/provider-accounts.mjs +124 -20
  253. package/src/runtime/shared/provider-api-key.mjs +12 -0
  254. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  255. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  256. package/src/runtime/shared/shell-display.mjs +9 -3
  257. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  258. package/src/runtime/shared/skill-mentions.mjs +133 -0
  259. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  260. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  261. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  262. package/src/runtime/shared/tool-primitives.mjs +35 -2
  263. package/src/runtime/shared/turn-changes.mjs +447 -219
  264. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  265. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  266. package/src/runtime/shared/turn-worktree-snapshot.mjs +160 -51
  267. package/src/runtime/shared/worker-requests.mjs +15 -2
  268. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  269. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  270. package/src/session-runtime/automation-agents.mjs +8 -2
  271. package/src/session-runtime/boot/apis.mjs +61 -0
  272. package/src/session-runtime/boot/config.mjs +5 -2
  273. package/src/session-runtime/boot/facade.mjs +1 -0
  274. package/src/session-runtime/boot/infrastructure.mjs +35 -18
  275. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  276. package/src/session-runtime/conversation-import/service.mjs +369 -0
  277. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  278. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  279. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  280. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  281. package/src/session-runtime/mcp-glue/connect.mjs +83 -16
  282. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  283. package/src/session-runtime/mcp-glue/server-catalog.mjs +135 -33
  284. package/src/session-runtime/mcp-glue.mjs +25 -2
  285. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  286. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  287. package/src/session-runtime/model-route/set-route.mjs +83 -15
  288. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  289. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  290. package/src/session-runtime/new-session-config.mjs +3 -2
  291. package/src/session-runtime/notification-bus.mjs +9 -2
  292. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  293. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  294. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  295. package/src/session-runtime/provider-readiness.mjs +16 -10
  296. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  297. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  298. package/src/session-runtime/resource-extension-sync.mjs +24 -5
  299. package/src/session-runtime/resource-mcp-api.mjs +198 -78
  300. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  301. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  302. package/src/session-runtime/resource-skills-api.mjs +23 -2
  303. package/src/session-runtime/runtime-review-api.mjs +13 -0
  304. package/src/session-runtime/schedule-session-run.mjs +1 -1
  305. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  306. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  307. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  308. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  309. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  310. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  311. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  312. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  313. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  314. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  315. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  316. package/src/session-runtime/services/provider-admin.mjs +64 -18
  317. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  318. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  319. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  320. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  321. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  322. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  323. package/src/session-runtime/skills-api.mjs +7 -3
  324. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  325. package/src/session-runtime/webhook-session-run.mjs +16 -4
  326. package/src/skill-script-entry.mjs +36 -0
  327. package/src/standalone/daemon-drain-policy.mjs +8 -0
  328. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  329. package/src/standalone/daemon-telemetry.mjs +55 -0
  330. package/src/standalone/daemon.mjs +44 -11
  331. package/src/standalone/session-protocol.mjs +12 -0
  332. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  333. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  334. package/src/standalone/session-service/agent-tree.mjs +12 -0
  335. package/src/standalone/session-service/quota-resume.mjs +8 -0
  336. package/src/standalone/session-service/retention/busy.mjs +15 -1
  337. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  338. package/src/standalone/session-service-api.mjs +5 -0
  339. package/src/standalone/session-service.mjs +1 -0
  340. package/src/tui/App.jsx +9 -1
  341. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  342. package/src/tui/app/app-view.jsx +7 -0
  343. package/src/tui/app/create-app-pickers.mjs +4 -0
  344. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  345. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  346. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  347. package/src/tui/app/extension-pickers.mjs +2 -1
  348. package/src/tui/app/model-options.mjs +13 -16
  349. package/src/tui/app/skill-mention-palette.mjs +88 -0
  350. package/src/tui/app/slash-commands.mjs +7 -2
  351. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  352. package/src/tui/app/slash-palette.mjs +23 -7
  353. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  354. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  355. package/src/tui/app/use-prompt-state.mjs +5 -0
  356. package/src/tui/components/Message.jsx +22 -1
  357. package/src/tui/components/PromptInput.jsx +21 -3
  358. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  359. package/src/tui/components/StatusLine.jsx +2 -0
  360. package/src/tui/components/TranscriptItem.jsx +9 -1
  361. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  362. package/src/tui/components/status-line/local-line.mjs +5 -2
  363. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  364. package/src/tui/components/user-message-skills.mjs +38 -0
  365. package/src/tui/dist/index.mjs +1323 -214
  366. package/src/tui/session/queue-helpers.mjs +7 -2
  367. package/src/tui/session/quota-resume.mjs +46 -14
  368. package/src/tui/session/session-api/commands.mjs +33 -24
  369. package/src/tui/session/session-api/extensions.mjs +26 -2
  370. package/src/tui/session/session-api/integrations.mjs +7 -0
  371. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  372. package/src/tui/session/session-api/routes.mjs +71 -7
  373. package/src/tui/session/session-api/settings.mjs +12 -14
  374. package/src/tui/session/session-flow/drain.mjs +21 -5
  375. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  376. package/src/tui/session/tui-steering-persist.mjs +25 -9
  377. package/src/tui/session/turn.mjs +55 -15
  378. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  379. package/src/ui/statusline-agents.mjs +15 -4
  380. package/src/ui/statusline.mjs +7 -3
  381. package/src/ui/streaming-markdown-heal.mjs +34 -3
  382. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  383. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  384. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
  385. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  386. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -1,15 +1,35 @@
1
- // Effective MCP server set and its status projection: config entries folded
2
- // with plugin ownership, per-server config reads, and the live/failed rollup.
1
+ // Effective MCP server set and its status projection: the user's configured
2
+ // servers plus the servers registered plugins declare (resolved live, with
3
+ // explicit owner metadata), per-server config reads, and the live/failed rollup.
3
4
  import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
4
5
  import { envFlag } from '../../runtime/shared/env.mjs';
6
+ import { resolveMcpServerCatalog } from './plugin-servers.mjs';
5
7
 
6
- function mcpServerStatus(cfg, live, fail) {
8
+ function mcpServerStatus(cfg, live, fail, loginPending) {
7
9
  if (cfg?.enabled === false) return 'disabled';
8
10
  if (live) return 'connected';
11
+ // Until an interactive sign-in completes the server still needs it.
12
+ if (loginPending || fail?.authRequired) return 'sign-in required';
9
13
  return fail ? 'failed' : 'disconnected';
10
14
  }
11
15
 
12
- export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, state }) {
16
+ const EMPTY_CATALOG = { servers: {}, sources: {}, entries: {}, plugins: {}, warnings: [], adoption: null };
17
+
18
+ // Internal markers on a resolved plugin config; never part of an editable config.
19
+ function withoutMarkers(cfg) {
20
+ const { _mixdogStrictEnv, _mixdogRootsFollowScope, _mixdogPlugin, ...rest } = cfg || {};
21
+ return rest;
22
+ }
23
+
24
+ export function createMcpServerCatalog({
25
+ mcpClient,
26
+ getConfig,
27
+ getCurrentCwd = () => '',
28
+ getMcpScopeId,
29
+ getPluginDataDir = () => '',
30
+ listPlugins = () => [],
31
+ state,
32
+ }) {
13
33
  function mcpTransportLabel(cfg = {}) {
14
34
  if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
15
35
  try {
@@ -19,33 +39,65 @@ export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, st
19
39
  }
20
40
  }
21
41
 
22
- function resolveEffectiveMcpServers() {
23
- // MCP is machine-global. Project `.mcp.json` files and per-project
24
- // overrides are intentionally outside the runtime resolution chain.
25
- if (envFlag('MIXDOG_DISABLE_MCP')) return { servers: {}, sources: {} };
26
- const config = getConfig();
27
- const configured = config?.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {};
28
- const servers = {};
29
- // A plugin-owned server is installed by enablePluginMcp and carries the
30
- // plugin root in its env; it belongs to the plugin's own toggle and stays
31
- // out of the standalone MCP list.
32
- const sources = {};
33
- for (const [name, cfg] of Object.entries(configured)) {
34
- servers[name] = {
35
- ...cfg,
36
- ...(cfg?._mixdogPluginDisabled === true ? { enabled: false } : {}),
37
- };
38
- sources[name] = cfg?.env?.MIXDOG_PLUGIN_ROOT ? 'plugin' : 'config';
42
+ /** Everything this runtime would connect, resolved now. MCP is
43
+ * machine-global: project `.mcp.json` files and per-project overrides stay
44
+ * outside the chain; only plugin servers without a declared cwd follow this
45
+ * runtime's session cwd. */
46
+ function resolveCatalog() {
47
+ if (envFlag('MIXDOG_DISABLE_MCP')) return EMPTY_CATALOG;
48
+ return resolveConfiguredCatalog();
49
+ }
50
+
51
+ /** Every configured server (user config plus plugin declarations, disabled
52
+ * ones included) whether or not this runtime connects MCP at all. */
53
+ function resolveConfiguredCatalog() {
54
+ let plugins = [];
55
+ let registryWarning = null;
56
+ try {
57
+ plugins = listPlugins() || [];
58
+ } catch (error) {
59
+ registryWarning = { message: `plugin MCP servers unavailable: ${error?.message || String(error)}` };
39
60
  }
61
+ const catalog = resolveMcpServerCatalog({
62
+ config: getConfig() || {},
63
+ plugins,
64
+ cwd: clean(getCurrentCwd?.()),
65
+ dataDir: clean(getPluginDataDir?.()),
66
+ });
67
+ if (registryWarning) catalog.warnings.unshift(registryWarning);
68
+ return catalog;
69
+ }
70
+
71
+ function resolveEffectiveMcpServers() {
72
+ const { servers, sources } = resolveCatalog();
40
73
  return { servers, sources };
41
74
  }
42
75
 
76
+ /** Plugin owner of an effective server name, or null for a configured one. */
77
+ function pluginServer(name) {
78
+ const catalog = resolveCatalog();
79
+ const entry = catalog.entries[clean(name)];
80
+ return entry?.owner ? { ...entry, plugin: catalog.plugins[entry.owner.id] } : null;
81
+ }
82
+
43
83
  function getMcpServerConfig(name) {
44
84
  const serverName = clean(name);
45
85
  if (!serverName) throw new Error('MCP server name is required');
46
- const { servers, sources } = resolveEffectiveMcpServers();
86
+ const { servers, sources, entries } = resolveCatalog();
47
87
  const effective = servers[serverName];
48
88
  if (!effective) throw new Error(`MCP server not configured: ${serverName}`);
89
+ // An unadopted plugin copy has an owner but stays the user's editable config.
90
+ const owner = sources[serverName] === 'plugin' ? entries[serverName]?.owner || null : null;
91
+ if (owner) {
92
+ return {
93
+ name: serverName,
94
+ source: 'plugin',
95
+ enabled: effective.enabled !== false,
96
+ owner,
97
+ readOnly: true,
98
+ config: withoutMarkers({ ...effective, enabled: undefined }),
99
+ };
100
+ }
49
101
  const raw = getConfig()?.mcpServers?.[serverName];
50
102
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
51
103
  throw new Error(`MCP server config is unavailable: ${serverName}`);
@@ -58,11 +110,8 @@ export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, st
58
110
  };
59
111
  }
60
112
 
61
- function mcpStatus() {
62
- if (envFlag('MIXDOG_DISABLE_MCP')) {
63
- return { servers: [], configuredCount: 0, connectedCount: 0, failedCount: 0 };
64
- }
65
- const { servers: configured, sources } = resolveEffectiveMcpServers();
113
+ function statusRows(catalog) {
114
+ const { servers: configured, sources, entries } = catalog;
66
115
  const connected = new Map((mcpClient.getMcpServerStatus?.(getMcpScopeId()) || []).map((row) => [row.name, row]));
67
116
  const failures = new Map((state.mcpFailures || []).map((row) => [row.name, row]));
68
117
  const servers = [];
@@ -73,32 +122,85 @@ export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, st
73
122
  const dead = live && (live.alive === false || live.connected === false);
74
123
  const fail =
75
124
  failures.get(name) || (dead ? { msg: live.error || live.lastError || 'MCP server is not alive' } : undefined);
76
- servers.push({
125
+ const entry = entries[name] || {};
126
+ const loginPending = !(live && !dead) && mcpClient.isMcpLoginPending?.(name, getMcpScopeId()) === true;
127
+ const status = mcpServerStatus(cfg, live && !dead, fail, loginPending);
128
+ // Needing an OAuth sign-in is a state, not an error.
129
+ const authRequired = status === 'sign-in required';
130
+ const row = {
77
131
  name,
78
132
  configured: true,
79
133
  enabled: cfg?.enabled !== false,
80
134
  connected: Boolean(live) && !dead,
81
- status: mcpServerStatus(cfg, live && !dead, fail),
135
+ status,
82
136
  transport: mcpTransportLabel(cfg),
83
137
  toolCount: live?.toolCount || 0,
84
138
  tools: live?.tools || [],
85
139
  capabilities: live?.capabilities || { tools: false, prompts: false, resources: false },
86
- error: fail?.msg || null,
140
+ error: authRequired ? null : fail?.msg || null,
141
+ warnings: [...(entry.warnings || []), ...(live?.warnings || [])],
87
142
  source: sources[name] || 'config',
88
- });
143
+ };
144
+ if (authRequired) row.authRequired = true;
145
+ const oauth = mcpClient.getMcpServerOAuthState?.(cfg);
146
+ // Why the last sign-in did not complete (already credential-scrubbed).
147
+ if (oauth) row.oauth = authRequired && fail?.msg ? { ...oauth, lastError: fail.msg } : oauth;
148
+ if (entry.owner) {
149
+ row.owner = entry.owner;
150
+ row.pluginEnabled = entry.pluginEnabled;
151
+ row.serverEnabled = entry.serverEnabled;
152
+ if (!entry.pluginEnabled) row.disabledReason = 'plugin disabled';
153
+ else if (!entry.serverEnabled) row.disabledReason = 'server disabled';
154
+ }
155
+ servers.push(row);
89
156
  connected.delete(name);
90
157
  }
91
158
  for (const live of connected.values()) {
92
159
  servers.push({ ...live, configured: false, status: 'connected' });
93
160
  }
94
161
  servers.sort((a, b) => String(a.name).localeCompare(String(b.name)));
162
+ return servers;
163
+ }
164
+
165
+ function mcpStatus() {
166
+ if (envFlag('MIXDOG_DISABLE_MCP')) {
167
+ return { servers: [], configuredCount: 0, connectedCount: 0, failedCount: 0 };
168
+ }
169
+ const catalog = resolveCatalog();
170
+ const servers = statusRows(catalog);
171
+ const declarationErrors = Object.values(catalog.plugins).flatMap((plugin) =>
172
+ plugin.errors.map((error) => ({ plugin: plugin.id, ...error }))
173
+ );
95
174
  return {
96
175
  servers,
97
- configuredCount: Object.keys(configured).length,
176
+ configuredCount: Object.keys(catalog.servers).length,
98
177
  connectedCount: servers.filter((row) => row.connected).length,
99
178
  failedCount: servers.filter((row) => row.status === 'failed').length,
179
+ warnings: catalog.warnings,
180
+ declarationErrors,
100
181
  };
101
182
  }
102
183
 
103
- return { mcpTransportLabel, resolveEffectiveMcpServers, getMcpServerConfig, mcpStatus };
184
+ /** Per plugin id: declaration format/errors and its server status rows. */
185
+ function pluginMcpSummary() {
186
+ const catalog = resolveCatalog();
187
+ const rows = new Map(statusRows(catalog).map((row) => [row.name, row]));
188
+ return new Map(
189
+ Object.values(catalog.plugins).map((plugin) => [
190
+ plugin.id,
191
+ { format: plugin.format, errors: plugin.errors, servers: plugin.servers.map((name) => rows.get(name)) },
192
+ ])
193
+ );
194
+ }
195
+
196
+ return {
197
+ mcpTransportLabel,
198
+ resolveCatalog,
199
+ resolveConfiguredCatalog,
200
+ resolveEffectiveMcpServers,
201
+ pluginServer,
202
+ getMcpServerConfig,
203
+ mcpStatus,
204
+ pluginMcpSummary,
205
+ };
104
206
  }
@@ -6,11 +6,28 @@ import { createMcpServerCatalog } from './mcp-glue/server-catalog.mjs';
6
6
  import { createMcpConnect } from './mcp-glue/connect.mjs';
7
7
  import { createMcpServerInput } from './mcp-glue/server-input.mjs';
8
8
 
9
- export function createMcpGlue({ mcpClient, getConfig, getCurrentCwd, getMcpScopeId = () => null, state }) {
10
- const catalog = createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, state });
9
+ export function createMcpGlue({
10
+ mcpClient,
11
+ getConfig,
12
+ getCurrentCwd,
13
+ getMcpScopeId = () => null,
14
+ getPluginDataDir,
15
+ listPlugins,
16
+ state,
17
+ }) {
18
+ const catalog = createMcpServerCatalog({
19
+ mcpClient,
20
+ getConfig,
21
+ getCurrentCwd,
22
+ getMcpScopeId,
23
+ getPluginDataDir,
24
+ listPlugins,
25
+ state,
26
+ });
11
27
  const connect = createMcpConnect({
12
28
  mcpClient,
13
29
  getMcpScopeId,
30
+ getCurrentCwd,
14
31
  state,
15
32
  resolveEffectiveMcpServers: catalog.resolveEffectiveMcpServers,
16
33
  mcpStatus: catalog.mcpStatus,
@@ -19,9 +36,15 @@ export function createMcpGlue({ mcpClient, getConfig, getCurrentCwd, getMcpScope
19
36
 
20
37
  return {
21
38
  mcpStatus: catalog.mcpStatus,
39
+ resolveMcpCatalog: catalog.resolveCatalog,
40
+ resolveConfiguredMcpCatalog: catalog.resolveConfiguredCatalog,
41
+ pluginMcpServer: catalog.pluginServer,
42
+ pluginMcpSummary: catalog.pluginMcpSummary,
22
43
  getMcpServerConfig: catalog.getMcpServerConfig,
23
44
  connectConfiguredMcp: connect.connectConfiguredMcp,
24
45
  awaitInitialMcpConnect: connect.awaitInitialMcpConnect,
46
+ cancelMcpLogin: connect.cancelMcpLogin,
47
+ completeMcpLogin: connect.completeMcpLogin,
25
48
  normalizeMcpServerInput,
26
49
  };
27
50
  }
@@ -0,0 +1,174 @@
1
+ // Explicit MCP OAuth sign-in flows: the only interactive MCP connect. A flow
2
+ // reconnects one configured server with browser login allowed, hands the
3
+ // authorization URL back to the caller (opening the host browser only when
4
+ // asked), and is then polled until the reconnect settles.
5
+ import { randomUUID } from 'node:crypto';
6
+ import { openInBrowser } from '../runtime/shared/open-url.mjs';
7
+
8
+ // Settled flows stay readable long enough for a poller to observe the result.
9
+ const SETTLED_FLOW_TTL_MS = 10 * 60_000;
10
+ const NOT_COMPLETED = 'MCP OAuth sign-in was not completed';
11
+
12
+ const NOT_PENDING = 'MCP OAuth sign-in is no longer pending';
13
+
14
+ // flowId -> flow, shared by every session runtime of this process. A caller
15
+ // is not bound to the runtime that began a flow (the desktop's control
16
+ // session is replaced after a failed call, and an idle one is evicted), so
17
+ // status / complete / cancel reach the flow from any runtime; each flow keeps
18
+ // the handlers of the runtime that began it (its MCP scope and connect).
19
+ const flows = new Map();
20
+
21
+ // A pending flow keeps its authorization URL readable: a client polling the
22
+ // status (remote sign-in) still shows the page to open.
23
+ const view = (flow) => ({
24
+ flowId: flow.flowId,
25
+ state: flow.state,
26
+ ...(flow.state === 'pending' && flow.url ? { url: flow.url } : {}),
27
+ ...(flow.error ? { error: flow.error } : {}),
28
+ });
29
+ function settle(flow, state, error = null) {
30
+ if (flow.state !== 'pending') return;
31
+ flow.state = state;
32
+ if (state === 'failed') flow.error = error || NOT_COMPLETED;
33
+ setTimeout(() => flows.delete(flow.flowId), SETTLED_FLOW_TTL_MS).unref?.();
34
+ }
35
+ function flowOf(flowId) {
36
+ const flow = flows.get(String(flowId || ''));
37
+ if (!flow) throw new Error('MCP OAuth login not found');
38
+ return flow;
39
+ }
40
+ async function cancelFlow(flow) {
41
+ if (flow.state === 'pending') {
42
+ flow.cancelled = true;
43
+ flow.owner.cancel(flow.name);
44
+ await flow.done;
45
+ }
46
+ return view(flow);
47
+ }
48
+
49
+ /**
50
+ * `connect(name, openBrowser)` reconnects the server interactively, calling
51
+ * `openBrowser(url)` when a login starts, and resolves `{ connected, error }`.
52
+ * `cancel(name)` aborts that server's pending login. `completeCallback(name,
53
+ * callbackUrl)` answers it with a pasted redirect URL (throws a fixed message
54
+ * for an invalid one). `onComplete(name)` runs after a successful sign-in,
55
+ * before the flow reports complete.
56
+ */
57
+ export function createMcpOAuthLogins({
58
+ connect,
59
+ cancel,
60
+ completeCallback = () => {
61
+ throw new Error(NOT_PENDING);
62
+ },
63
+ onComplete = async () => {},
64
+ openBrowser = openInBrowser,
65
+ }) {
66
+ // name -> tail of that server's serialized begin/cancel/sign-out steps, so
67
+ // replacing a pending flow is atomic and exactly one can be pending.
68
+ const lanes = new Map();
69
+ function exclusive(name, step) {
70
+ const run = (lanes.get(name) || Promise.resolve()).then(step);
71
+ const tail = run.then(
72
+ () => {},
73
+ () => {}
74
+ );
75
+ lanes.set(name, tail);
76
+ tail.then(() => {
77
+ if (lanes.get(name) === tail) lanes.delete(name);
78
+ });
79
+ return run;
80
+ }
81
+ // This runtime's handlers, kept on each flow it begins.
82
+ const owner = {
83
+ exclusive,
84
+ cancel,
85
+ completeCallback,
86
+ };
87
+ // One sign-in per server of this runtime (its MCP scope).
88
+ const cancelServerFlows = (name) =>
89
+ Promise.all([...flows.values()].filter((flow) => flow.owner === owner && flow.name === name).map(cancelFlow));
90
+
91
+ async function begin(name, options = {}) {
92
+ const { openBrowser: hostBrowser } = options || {};
93
+ if (hostBrowser !== undefined && typeof hostBrowser !== 'boolean') {
94
+ throw new TypeError('openBrowser must be a boolean');
95
+ }
96
+ let announceUrl;
97
+ const urlReady = new Promise((resolve) => {
98
+ announceUrl = resolve;
99
+ });
100
+ // One sign-in per server: a new one replaces the pending one, and the
101
+ // replacement is registered before the next queued step of this server.
102
+ const flow = await exclusive(name, async () => {
103
+ await cancelServerFlows(name);
104
+ const flow = {
105
+ flowId: `mcp_oauth_${randomUUID()}`,
106
+ name,
107
+ owner,
108
+ state: 'pending',
109
+ error: null,
110
+ cancelled: false,
111
+ url: null,
112
+ };
113
+ flows.set(flow.flowId, flow);
114
+ const launch = async (url) => {
115
+ // A login reached after its flow was cancelled (queued behind another
116
+ // connect of this server) never opens anything.
117
+ if (flow.cancelled) throw new Error('MCP OAuth login cancelled');
118
+ flow.awaitingCallback = true;
119
+ flow.url = url;
120
+ announceUrl(url);
121
+ if (hostBrowser !== false) openBrowser(url);
122
+ };
123
+ flow.done = (async () => {
124
+ try {
125
+ const outcome = await connect(name, launch);
126
+ if (flow.cancelled) settle(flow, 'cancelled');
127
+ else if (outcome?.connected) {
128
+ // The sign-in itself succeeded; a failed follow-up never undoes it.
129
+ await Promise.resolve()
130
+ .then(() => onComplete(name))
131
+ .catch(() => {});
132
+ settle(flow, 'complete');
133
+ } else settle(flow, 'failed', outcome?.error);
134
+ } catch (error) {
135
+ settle(flow, flow.cancelled ? 'cancelled' : 'failed', error?.message || String(error));
136
+ }
137
+ })();
138
+ return flow;
139
+ });
140
+ const url = await Promise.race([urlReady, flow.done.then(() => null)]);
141
+ if (url) return { flowId: flow.flowId, state: 'pending', url };
142
+ // Settled without a login: stored credentials (or no challenge) sufficed.
143
+ if (flow.state === 'complete') return view(flow);
144
+ // A superseded or failed begin stays readable (cancelled / failed).
145
+ throw new Error(flow.error || NOT_COMPLETED);
146
+ }
147
+
148
+ return {
149
+ begin,
150
+ status: (flowId) => view(flowOf(flowId)),
151
+ cancel(flowId) {
152
+ const flow = flowOf(flowId);
153
+ return flow.owner.exclusive(flow.name, () => cancelFlow(flow));
154
+ },
155
+ /** Answer the pending flow with the redirect URL pasted from the browser
156
+ * and resolve once it settles: `{ flowId, state: 'complete' | 'failed',
157
+ * error? }`. An invalid paste throws and leaves the flow pending. */
158
+ async complete(flowId, callbackUrl) {
159
+ const flow = flowOf(flowId);
160
+ if (flow.state !== 'pending' || flow.cancelled || !flow.awaitingCallback) throw new Error(NOT_PENDING);
161
+ flow.owner.completeCallback(flow.name, callbackUrl);
162
+ await flow.done;
163
+ return view(flow);
164
+ },
165
+ /** Cancel every pending sign-in of one server, then run `after` (sign-out)
166
+ * before any later begin of that server starts; resolves its result. */
167
+ cancelServer(name, after = async () => {}) {
168
+ return exclusive(name, async () => {
169
+ await cancelServerFlows(name);
170
+ return after();
171
+ });
172
+ },
173
+ };
174
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * model-switch-target.mjs — what a user's model selection names, and whether
3
+ * it switches an established conversation to another model. Shared by every
4
+ * surface that offers the choice (session API routes, plain REPL /model).
5
+ */
6
+
7
+ /** The provider/model a selection names; a bare preset id or name names the
8
+ * preset's model. */
9
+ export function selectedModel(runtime, request, sessionProvider, sessionModel) {
10
+ const model = String(request?.model ?? '').trim();
11
+ if (!request?.provider && model) {
12
+ const wanted = model.toLowerCase();
13
+ const presets = runtime?.listPresets?.();
14
+ const preset = (Array.isArray(presets) ? presets : []).find((entry) =>
15
+ [entry?.id, entry?.name].some(
16
+ (key) =>
17
+ String(key ?? '')
18
+ .trim()
19
+ .toLowerCase() === wanted
20
+ )
21
+ );
22
+ if (preset?.provider && preset?.model) return { provider: String(preset.provider), model: String(preset.model) };
23
+ }
24
+ return { provider: String(request?.provider ?? sessionProvider), model: model || sessionModel };
25
+ }
26
+
27
+ /** A selection of another model than the established conversation's own. */
28
+ export function switchesSessionModel(runtime, request) {
29
+ const sessionProvider = String(runtime?.sessionProvider ?? '');
30
+ const sessionModel = String(runtime?.sessionModel ?? '');
31
+ if (!sessionProvider || !sessionModel) return false;
32
+ const { provider, model } = selectedModel(runtime, request, sessionProvider, sessionModel);
33
+ return provider !== sessionProvider || model !== sessionModel;
34
+ }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * set-route.mjs — select the main route: resolve and validate the request,
3
3
  * adopt it into config, then decide how the current session follows it
4
- * (untouched, tuned, recreated, or updated in place when empty).
4
+ * (untouched, tuned, recreated, updated in place when empty, or switched in
5
+ * place when the selection is addressed to an established conversation).
5
6
  */
6
7
  import { clean, hasOwn } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
7
8
  import {
@@ -117,6 +118,51 @@ function updateSessionInPlace(deps, session, updateOptions) {
117
118
  writeStatuslineRoute(statusRoutes, getSession(), route);
118
119
  }
119
120
 
121
+ /** Index of the message a model switch boundary is recorded on: the last one
122
+ * before a trailing unanswered prompt, which a retry may rewind. */
123
+ function modelSwitchAnchorIndex(messages) {
124
+ let index = messages.length - 1;
125
+ while (index >= 0 && (messages[index]?.role === 'user' || messages[index]?.role === 'system')) index -= 1;
126
+ if (index >= 0) return index;
127
+ return messages.findLastIndex((message) => message?.role !== 'system');
128
+ }
129
+
130
+ /** Display-only boundary of an in-place model switch, kept in message meta
131
+ * (like an inheritance boundary): the transcript shows it live and after a
132
+ * restore, and the model conversation gains no synthetic message. */
133
+ export function recordModelSwitchBoundary(session, boundary) {
134
+ const messages = Array.isArray(session?.messages) ? session.messages : [];
135
+ const index = modelSwitchAnchorIndex(messages);
136
+ if (index < 0) return false;
137
+ const anchor = messages[index];
138
+ const switches = Array.isArray(anchor.meta?.modelSwitches) ? anchor.meta.modelSwitches : [];
139
+ if (switches.some((entry) => entry?.at === boundary.at && entry?.modelId === boundary.modelId)) return true;
140
+ // Replace the message object: the session save worker detects changed
141
+ // messages by reference identity, so an in-place meta edit is never saved.
142
+ messages[index] = { ...anchor, meta: { ...anchor.meta, modelSwitches: [...switches, boundary] } };
143
+ return true;
144
+ }
145
+
146
+ /** Move the addressed, established session onto the adopted route in place.
147
+ * Its provider replay state and context readings are dropped, so the next
148
+ * turn re-measures and auto-compacts against the new model's window. Returns
149
+ * the recorded boundary, or null when the session has nothing to mark. */
150
+ function switchSessionInPlace(deps, session) {
151
+ const { getRoute, getSession } = deps;
152
+ const route = getRoute();
153
+ if (route.provider === session.provider && route.model === session.model) {
154
+ updateSessionInPlace(deps, session);
155
+ return null;
156
+ }
157
+ const boundary = { provider: route.provider, modelId: route.model, at: Date.now() };
158
+ const recorded = recordModelSwitchBoundary(session, boundary);
159
+ updateSessionInPlace(deps, session, { crossProvider: true });
160
+ // A manager that answered with another copy of the session carries the
161
+ // boundary too; the next save persists it.
162
+ if (recorded && getSession() && getSession() !== session) recordModelSwitchBoundary(getSession(), boundary);
163
+ return recorded ? boundary : null;
164
+ }
165
+
120
166
  /** Validate, persist and adopt the requested route as the runtime's route. */
121
167
  async function adoptRoute(deps, persist, next, options) {
122
168
  const {
@@ -144,7 +190,9 @@ async function adoptRoute(deps, persist, next, options) {
144
190
  * Quota-exhaustion escape hatch: apply `next` to the addressed, ESTABLISHED
145
191
  * session in place. The caller (session API) has already proven the latest
146
192
  * settled turn failed on quota and that nothing is running. Starts no turn;
147
- * the next turn's auto-compaction fits history to the new window.
193
+ * the next turn's auto-compaction fits history to the new window. Answers
194
+ * `{ modelSwitch }` with the recorded boundary (as setRoute does) so the
195
+ * session surface shows the same live divider, or true when nothing switched.
148
196
  */
149
197
  export function createContinueSessionInModel(deps, persist) {
150
198
  const { getSession, invalidateContextStatusCache } = deps;
@@ -154,33 +202,53 @@ export function createContinueSessionInModel(deps, persist) {
154
202
  throw new Error(`session ${sessionId || ''} is not the active session`);
155
203
  }
156
204
  await adoptRoute(deps, persist, next, {});
157
- updateSessionInPlace(deps, session, { crossProvider: true });
205
+ const modelSwitch = switchSessionInPlace(deps, session);
158
206
  invalidateContextStatusCache();
159
- return true;
207
+ return modelSwitch ? { modelSwitch } : true;
160
208
  };
161
209
  }
162
210
 
211
+ /** Rejects an unacceptable selection without applying it. */
212
+ async function validateSelectedRoute(deps, next) {
213
+ await resolveSelectedRoute(deps, next);
214
+ return deps.getRoute();
215
+ }
216
+
163
217
  export function createSetRoute(deps, persist) {
164
218
  const { getRoute, getSession, invalidateContextStatusCache, createCurrentSession } = deps;
165
219
 
166
220
  return async function setRoute(next, options = {}) {
167
- // Selection is for a new/inherited session. An explicit durable address
168
- // permits initializing an EMPTY session, never rewriting an existing
169
- // conversation's model, context boundary or provider cache.
221
+ // Without an explicit durable address a selection is for a new/inherited
222
+ // session and never rewrites an existing conversation's model, unless it
223
+ // is the user's model choice for this conversation (`switchEstablished`,
224
+ // the session surface): that one switches an established conversation in
225
+ // place while an empty one follows the unaddressed rules below.
170
226
  const applyToCurrentSession = options?.applyToCurrentSession === true;
227
+ const switchEstablished = applyToCurrentSession || options?.switchEstablished === true;
228
+ // Lets a caller prove the selection is acceptable before it does
229
+ // irreversible work (compaction) ahead of the switch; changes nothing.
230
+ if (options?.validateOnly === true) return validateSelectedRoute(deps, next);
171
231
  await adoptRoute(deps, persist, next, options);
172
232
  const session = getSession();
173
- // Model/provider changes are next-session-only for a session the user
174
- // has already talked in or compacted (provider-keyed prompt cache). But
175
- // a current session with no model reply, no compaction summary and no
176
- // in-flight first turn — empty, or holding only refused prompts — has no
177
- // cache to protect, so /model takes effect live: route + statusline
233
+ // A session the user has already talked in or compacted keeps its route
234
+ // unless the selection was addressed to it. A current session with no
235
+ // model reply, no compaction summary and no in-flight first turn — empty,
236
+ // or holding only refused prompts — takes effect live: route + statusline
178
237
  // update immediately. Only a truly empty session is recreated below.
179
238
  const currentSessionEmpty = !!session && !sessionRouteCommitted(session);
180
239
  if (!currentSessionEmpty) {
181
- // Some desktop effort changes travel as a same-model route update.
182
- // Only effort/Fast may affect the source, starting with its next turn.
183
- if (applyToCurrentSession) persist.applySessionTuning();
240
+ if (!switchEstablished) return getRoute();
241
+ const route = getRoute();
242
+ // An addressed selection of another model switches the conversation
243
+ // itself in place; the caller (session surface) refuses it while a
244
+ // turn runs. A same-model update (effort/Fast) only tunes the session
245
+ // from its next turn.
246
+ if (session && (route.provider !== session.provider || route.model !== session.model)) {
247
+ const modelSwitch = switchSessionInPlace(deps, session);
248
+ invalidateContextStatusCache();
249
+ return modelSwitch ? { ...getRoute(), modelSwitch } : getRoute();
250
+ }
251
+ persist.applySessionTuning();
184
252
  return getRoute();
185
253
  }
186
254
  // An addressed runtime keeps its durable id: the empty session is updated
@@ -62,7 +62,7 @@ export function createWebSearchRouteApi(deps) {
62
62
  await awaitKeychainPrewarm();
63
63
  ensureFullConfig();
64
64
  await ensureProvidersReady(ensureProviderEnabled(getConfig(), selectedRoute.provider));
65
- const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
65
+ const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model, { allowFetch: true });
66
66
  if (!webSearchCapableFor(selectedRoute.provider, modelMeta)) {
67
67
  throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
68
68
  }