mixdog 1.2.0 → 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 (201) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -0
  4. package/src/defaults/skills/pptx/SKILL.md +6 -6
  5. package/src/defaults/skills/pptx/references/review.md +6 -4
  6. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  7. package/src/defaults/skills/setup/references/actions.md +13 -3
  8. package/src/defaults/skills/visualize/SKILL.md +12 -9
  9. package/src/rules/shared/60-verification.md +3 -1
  10. package/src/rules/shared/70-delivery.md +4 -2
  11. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  12. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  13. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  14. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  15. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  16. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
  17. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
  18. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
  19. package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
  20. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  21. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
  25. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  27. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  28. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  29. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  31. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  33. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  34. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  35. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  36. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  37. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  38. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  43. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
  44. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  45. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  46. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  48. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  49. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
  50. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  53. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  54. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  55. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  56. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  57. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  58. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  59. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  60. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  61. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  62. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  63. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  64. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  65. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  66. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  68. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  70. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  71. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
  73. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  76. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  77. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  78. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  79. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  80. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  82. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
  84. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  85. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  86. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  87. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
  89. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  91. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  92. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  99. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  102. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  104. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  105. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  106. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  107. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  108. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  110. package/src/runtime/attachments/store.d.mts +3 -0
  111. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  112. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  113. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  114. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  115. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  116. package/src/runtime/memory/lib/memory.mjs +1 -1
  117. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  118. package/src/runtime/shared/atomic-file.mjs +25 -4
  119. package/src/runtime/shared/extension-scopes.mjs +5 -3
  120. package/src/runtime/shared/file-lock.mjs +37 -11
  121. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  122. package/src/runtime/shared/llm/model-catalog.mjs +0 -12
  123. package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
  124. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  125. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  126. package/src/runtime/shared/pristine-execution.mjs +28 -4
  127. package/src/runtime/shared/provider-accounts.mjs +124 -20
  128. package/src/runtime/shared/provider-api-key.mjs +12 -0
  129. package/src/runtime/shared/shell-display.mjs +9 -3
  130. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  131. package/src/runtime/shared/skill-mentions.mjs +133 -0
  132. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  133. package/src/runtime/shared/tool-primitives.mjs +35 -2
  134. package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
  135. package/src/runtime/shared/worker-requests.mjs +15 -2
  136. package/src/session-runtime/automation-agents.mjs +8 -2
  137. package/src/session-runtime/boot/apis.mjs +55 -0
  138. package/src/session-runtime/boot/config.mjs +5 -2
  139. package/src/session-runtime/boot/facade.mjs +1 -0
  140. package/src/session-runtime/boot/infrastructure.mjs +29 -18
  141. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  142. package/src/session-runtime/conversation-import/service.mjs +369 -0
  143. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  144. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  145. package/src/session-runtime/mcp-glue/connect.mjs +57 -10
  146. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  147. package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
  148. package/src/session-runtime/mcp-glue.mjs +22 -2
  149. package/src/session-runtime/new-session-config.mjs +3 -2
  150. package/src/session-runtime/notification-bus.mjs +9 -2
  151. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  152. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  153. package/src/session-runtime/provider-readiness.mjs +16 -10
  154. package/src/session-runtime/resource-extension-sync.mjs +20 -5
  155. package/src/session-runtime/resource-mcp-api.mjs +101 -77
  156. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  157. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  158. package/src/session-runtime/resource-skills-api.mjs +23 -2
  159. package/src/session-runtime/schedule-session-run.mjs +1 -1
  160. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  161. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  162. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  163. package/src/session-runtime/services/provider-admin.mjs +64 -18
  164. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  165. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  166. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  167. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  168. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  169. package/src/session-runtime/skills-api.mjs +7 -3
  170. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  171. package/src/session-runtime/webhook-session-run.mjs +16 -4
  172. package/src/skill-script-entry.mjs +36 -0
  173. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  174. package/src/standalone/daemon.mjs +41 -4
  175. package/src/standalone/session-protocol.mjs +5 -0
  176. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  177. package/src/tui/App.jsx +9 -1
  178. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  179. package/src/tui/app/app-view.jsx +6 -0
  180. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  181. package/src/tui/app/model-options.mjs +2 -0
  182. package/src/tui/app/skill-mention-palette.mjs +88 -0
  183. package/src/tui/app/slash-palette.mjs +23 -7
  184. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  185. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  186. package/src/tui/app/use-prompt-state.mjs +5 -0
  187. package/src/tui/components/Message.jsx +22 -1
  188. package/src/tui/components/PromptInput.jsx +21 -3
  189. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  190. package/src/tui/components/TranscriptItem.jsx +9 -1
  191. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  192. package/src/tui/components/user-message-skills.mjs +38 -0
  193. package/src/tui/dist/index.mjs +507 -171
  194. package/src/tui/session/queue-helpers.mjs +7 -2
  195. package/src/tui/session/session-api/extensions.mjs +4 -2
  196. package/src/tui/session/session-api/settings.mjs +7 -0
  197. package/src/tui/session/session-flow/drain.mjs +14 -4
  198. package/src/tui/session/tui-steering-persist.mjs +25 -9
  199. package/src/tui/session/turn.mjs +17 -0
  200. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  201. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -1,7 +1,9 @@
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
8
  function mcpServerStatus(cfg, live, fail) {
7
9
  if (cfg?.enabled === false) return 'disabled';
@@ -9,7 +11,23 @@ function mcpServerStatus(cfg, live, fail) {
9
11
  return fail ? 'failed' : 'disconnected';
10
12
  }
11
13
 
12
- export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, state }) {
14
+ const EMPTY_CATALOG = { servers: {}, sources: {}, entries: {}, plugins: {}, warnings: [], adoption: null };
15
+
16
+ // Internal markers on a resolved plugin config; never part of an editable config.
17
+ function withoutMarkers(cfg) {
18
+ const { _mixdogStrictEnv, _mixdogRootsFollowScope, _mixdogPlugin, ...rest } = cfg || {};
19
+ return rest;
20
+ }
21
+
22
+ export function createMcpServerCatalog({
23
+ mcpClient,
24
+ getConfig,
25
+ getCurrentCwd = () => '',
26
+ getMcpScopeId,
27
+ getPluginDataDir = () => '',
28
+ listPlugins = () => [],
29
+ state,
30
+ }) {
13
31
  function mcpTransportLabel(cfg = {}) {
14
32
  if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
15
33
  try {
@@ -19,33 +37,59 @@ export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, st
19
37
  }
20
38
  }
21
39
 
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';
40
+ /** Everything this runtime would connect, resolved now. MCP is
41
+ * machine-global: project `.mcp.json` files and per-project overrides stay
42
+ * outside the chain; only plugin servers without a declared cwd follow this
43
+ * runtime's session cwd. */
44
+ function resolveCatalog() {
45
+ if (envFlag('MIXDOG_DISABLE_MCP')) return EMPTY_CATALOG;
46
+ let plugins = [];
47
+ let registryWarning = null;
48
+ try {
49
+ plugins = listPlugins() || [];
50
+ } catch (error) {
51
+ registryWarning = { message: `plugin MCP servers unavailable: ${error?.message || String(error)}` };
39
52
  }
53
+ const catalog = resolveMcpServerCatalog({
54
+ config: getConfig() || {},
55
+ plugins,
56
+ cwd: clean(getCurrentCwd?.()),
57
+ dataDir: clean(getPluginDataDir?.()),
58
+ });
59
+ if (registryWarning) catalog.warnings.unshift(registryWarning);
60
+ return catalog;
61
+ }
62
+
63
+ function resolveEffectiveMcpServers() {
64
+ const { servers, sources } = resolveCatalog();
40
65
  return { servers, sources };
41
66
  }
42
67
 
68
+ /** Plugin owner of an effective server name, or null for a configured one. */
69
+ function pluginServer(name) {
70
+ const catalog = resolveCatalog();
71
+ const entry = catalog.entries[clean(name)];
72
+ return entry?.owner ? { ...entry, plugin: catalog.plugins[entry.owner.id] } : null;
73
+ }
74
+
43
75
  function getMcpServerConfig(name) {
44
76
  const serverName = clean(name);
45
77
  if (!serverName) throw new Error('MCP server name is required');
46
- const { servers, sources } = resolveEffectiveMcpServers();
78
+ const { servers, sources, entries } = resolveCatalog();
47
79
  const effective = servers[serverName];
48
80
  if (!effective) throw new Error(`MCP server not configured: ${serverName}`);
81
+ // An unadopted plugin copy has an owner but stays the user's editable config.
82
+ const owner = sources[serverName] === 'plugin' ? entries[serverName]?.owner || null : null;
83
+ if (owner) {
84
+ return {
85
+ name: serverName,
86
+ source: 'plugin',
87
+ enabled: effective.enabled !== false,
88
+ owner,
89
+ readOnly: true,
90
+ config: withoutMarkers({ ...effective, enabled: undefined }),
91
+ };
92
+ }
49
93
  const raw = getConfig()?.mcpServers?.[serverName];
50
94
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
51
95
  throw new Error(`MCP server config is unavailable: ${serverName}`);
@@ -58,11 +102,8 @@ export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, st
58
102
  };
59
103
  }
60
104
 
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();
105
+ function statusRows(catalog) {
106
+ const { servers: configured, sources, entries } = catalog;
66
107
  const connected = new Map((mcpClient.getMcpServerStatus?.(getMcpScopeId()) || []).map((row) => [row.name, row]));
67
108
  const failures = new Map((state.mcpFailures || []).map((row) => [row.name, row]));
68
109
  const servers = [];
@@ -73,7 +114,8 @@ export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, st
73
114
  const dead = live && (live.alive === false || live.connected === false);
74
115
  const fail =
75
116
  failures.get(name) || (dead ? { msg: live.error || live.lastError || 'MCP server is not alive' } : undefined);
76
- servers.push({
117
+ const entry = entries[name] || {};
118
+ const row = {
77
119
  name,
78
120
  configured: true,
79
121
  enabled: cfg?.enabled !== false,
@@ -84,21 +126,64 @@ export function createMcpServerCatalog({ mcpClient, getConfig, getMcpScopeId, st
84
126
  tools: live?.tools || [],
85
127
  capabilities: live?.capabilities || { tools: false, prompts: false, resources: false },
86
128
  error: fail?.msg || null,
129
+ warnings: [...(entry.warnings || []), ...(live?.warnings || [])],
87
130
  source: sources[name] || 'config',
88
- });
131
+ };
132
+ if (entry.owner) {
133
+ row.owner = entry.owner;
134
+ row.pluginEnabled = entry.pluginEnabled;
135
+ row.serverEnabled = entry.serverEnabled;
136
+ if (!entry.pluginEnabled) row.disabledReason = 'plugin disabled';
137
+ else if (!entry.serverEnabled) row.disabledReason = 'server disabled';
138
+ }
139
+ servers.push(row);
89
140
  connected.delete(name);
90
141
  }
91
142
  for (const live of connected.values()) {
92
143
  servers.push({ ...live, configured: false, status: 'connected' });
93
144
  }
94
145
  servers.sort((a, b) => String(a.name).localeCompare(String(b.name)));
146
+ return servers;
147
+ }
148
+
149
+ function mcpStatus() {
150
+ if (envFlag('MIXDOG_DISABLE_MCP')) {
151
+ return { servers: [], configuredCount: 0, connectedCount: 0, failedCount: 0 };
152
+ }
153
+ const catalog = resolveCatalog();
154
+ const servers = statusRows(catalog);
155
+ const declarationErrors = Object.values(catalog.plugins).flatMap((plugin) =>
156
+ plugin.errors.map((error) => ({ plugin: plugin.id, ...error }))
157
+ );
95
158
  return {
96
159
  servers,
97
- configuredCount: Object.keys(configured).length,
160
+ configuredCount: Object.keys(catalog.servers).length,
98
161
  connectedCount: servers.filter((row) => row.connected).length,
99
162
  failedCount: servers.filter((row) => row.status === 'failed').length,
163
+ warnings: catalog.warnings,
164
+ declarationErrors,
100
165
  };
101
166
  }
102
167
 
103
- return { mcpTransportLabel, resolveEffectiveMcpServers, getMcpServerConfig, mcpStatus };
168
+ /** Per plugin id: declaration format/errors and its server status rows. */
169
+ function pluginMcpSummary() {
170
+ const catalog = resolveCatalog();
171
+ const rows = new Map(statusRows(catalog).map((row) => [row.name, row]));
172
+ return new Map(
173
+ Object.values(catalog.plugins).map((plugin) => [
174
+ plugin.id,
175
+ { format: plugin.format, errors: plugin.errors, servers: plugin.servers.map((name) => rows.get(name)) },
176
+ ])
177
+ );
178
+ }
179
+
180
+ return {
181
+ mcpTransportLabel,
182
+ resolveCatalog,
183
+ resolveEffectiveMcpServers,
184
+ pluginServer,
185
+ getMcpServerConfig,
186
+ mcpStatus,
187
+ pluginMcpSummary,
188
+ };
104
189
  }
@@ -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,6 +36,9 @@ export function createMcpGlue({ mcpClient, getConfig, getCurrentCwd, getMcpScope
19
36
 
20
37
  return {
21
38
  mcpStatus: catalog.mcpStatus,
39
+ resolveMcpCatalog: catalog.resolveCatalog,
40
+ pluginMcpServer: catalog.pluginServer,
41
+ pluginMcpSummary: catalog.pluginMcpSummary,
22
42
  getMcpServerConfig: catalog.getMcpServerConfig,
23
43
  connectConfiguredMcp: connect.connectConfiguredMcp,
24
44
  awaitInitialMcpConnect: connect.awaitInitialMcpConnect,
@@ -21,7 +21,8 @@ export function createNewSessionConfig({
21
21
  await flushPendingSessionConfigWrites();
22
22
  await sharedCfgMod.pendingConfigWrites();
23
23
  sharedCfgMod.invalidateConfigReadCache();
24
- const previousMcp = rt.config.mcpServers;
24
+ const mcpSettings = () => [rt.config.mcpServers, rt.config.pluginMcp];
25
+ const previousMcp = mcpSettings();
25
26
  // Headless callers inject a deliberately isolated config. Never replace
26
27
  // their explicit policy with the interactive profile on disk.
27
28
  if (!initialConfig || typeof initialConfig !== 'object') {
@@ -34,7 +35,7 @@ export function createNewSessionConfig({
34
35
  invalidatePreSessionToolSurface();
35
36
  invalidateSkills();
36
37
  await configureEmbedding(sharedCfgMod.readSection('memory')?.embedding || {});
37
- if (!firstSession || !isDeepStrictEqual(previousMcp, rt.config.mcpServers)) {
38
+ if (!firstSession || !isDeepStrictEqual(previousMcp, mcpSettings())) {
38
39
  // Reused runtimes may still own old connections. This reset is scoped to
39
40
  // the runtime that is constructing the new session, not to its peers.
40
41
  await connectConfiguredMcp({ reset: true });
@@ -3,7 +3,10 @@
3
3
  // pending queue so the model sees it next turn. The delivered registry keeps
4
4
  // those two paths from double-injecting the same completion.
5
5
  import { modelVisibleToolCompletionMessage } from '../runtime/shared/tool-execution-contract.mjs';
6
- import { markCompletionEntry } from '../runtime/agent/orchestrator/session/manager/pending-messages.mjs';
6
+ import {
7
+ PENDING_MODE_TASK_NOTIFICATION,
8
+ markCompletionEntry,
9
+ } from '../runtime/agent/orchestrator/session/manager/pending-messages.mjs';
7
10
  import {
8
11
  isDeliveredCompletion,
9
12
  logDuplicateSkip,
@@ -144,7 +147,11 @@ export function createNotificationBus({ listeners, mgr, onCompletionQueued, getO
144
147
  if (!open) return false;
145
148
  if (typeof mgr.enqueuePendingMessage !== 'function') return false;
146
149
  try {
147
- return mgr.enqueuePendingMessage(targetSessionId, content) > 0;
150
+ // A channel notification (webhook event rules, schedule injects, relays)
151
+ // is the runtime reporting, exactly as an attached TUI queues it: a
152
+ // task notification, so text inside it — fenced external webhook values
153
+ // included — never drives explicit skill selection.
154
+ return mgr.enqueuePendingMessage(targetSessionId, { content, mode: PENDING_MODE_TASK_NOTIFICATION }) > 0;
148
155
  } catch {
149
156
  return false;
150
157
  }
@@ -1,6 +1,10 @@
1
1
  // provider-auth/accounts.mjs — the per-provider account roster and account
2
2
  // switching, with the bounded background usage sweep that refreshes quotas.
3
- import { listProviderAccounts, updateProviderAccounts } from '../services/provider-admin.mjs';
3
+ import {
4
+ listProviderAccounts,
5
+ materializeProviderAccounts,
6
+ updateProviderAccounts,
7
+ } from '../services/provider-admin.mjs';
4
8
  import { getProvider } from '../../runtime/agent/orchestrator/providers/registry.mjs';
5
9
  import { fetchOAuthUsageSnapshot } from '../../runtime/agent/orchestrator/providers/oauth-usage.mjs';
6
10
 
@@ -11,21 +15,23 @@ export function createAccountApi(
11
15
  // One in-flight usage sweep per provider; a reopened picker reuses it.
12
16
  const accountUsageSweeps = new Map();
13
17
 
18
+ async function measureAccountUsage(providerId, provider, accounts, force = false) {
19
+ await awaitKeychainPrewarm();
20
+ for (let offset = 0; offset < accounts.length; offset += 4) {
21
+ await Promise.all(
22
+ accounts.slice(offset, offset + 4).map((account) => {
23
+ const target = { provider: providerId, accountId: account.id };
24
+ return fetchOAuthUsageSnapshot(target, provider.forAccount(account.id), () => {}, { force }).catch(
25
+ () => null
26
+ );
27
+ })
28
+ );
29
+ }
30
+ }
31
+
14
32
  async function sweepAccountUsage(providerId, provider, accounts) {
15
33
  try {
16
- await awaitKeychainPrewarm();
17
- for (let offset = 0; offset < accounts.length; offset += 4) {
18
- await Promise.all(
19
- accounts
20
- .slice(offset, offset + 4)
21
- .map((account) =>
22
- fetchOAuthUsageSnapshot(
23
- { provider: providerId, accountId: account.id },
24
- provider.forAccount(account.id)
25
- ).catch(() => null)
26
- )
27
- );
28
- }
34
+ await measureAccountUsage(providerId, provider, accounts);
29
35
  } finally {
30
36
  accountUsageSweeps.delete(providerId);
31
37
  }
@@ -69,8 +75,20 @@ export function createAccountApi(
69
75
  return pool;
70
76
  },
71
77
  async updateProviderAccounts(providerId, change) {
78
+ // `{ refreshUsage: true }` re-measures every account now (the roster's
79
+ // "Refresh usage") and changes nothing else in the roster. The implicit
80
+ // signed-in account is stored first so its measurement has a row to land on.
81
+ if (change?.refreshUsage === true && Object.keys(change).length === 1) {
82
+ const provider = getProvider(providerId);
83
+ if (typeof provider?.forAccount === 'function') {
84
+ await awaitKeychainPrewarm();
85
+ const { accounts } = await materializeProviderAccounts(providerId);
86
+ await measureAccountUsage(providerId, provider, accounts, true);
87
+ }
88
+ return listProviderAccounts(providerId);
89
+ }
72
90
  await awaitKeychainPrewarm();
73
- const result = updateProviderAccounts(providerId, change);
91
+ const result = await updateProviderAccounts(providerId, change);
74
92
  reloadFullConfig();
75
93
  invalidateProviderCaches();
76
94
  warmProviderModelCache();
@@ -38,7 +38,11 @@ export function createWebSearchCatalog({
38
38
  try {
39
39
  let models = null;
40
40
  if (forceRefresh && typeof provider._refreshModelCache === 'function') {
41
- models = await provider._refreshModelCache();
41
+ try {
42
+ models = await provider._refreshModelCache();
43
+ } catch {
44
+ models = null; // fall back to listModels below
45
+ }
42
46
  }
43
47
  if (!Array.isArray(models)) models = await provider.listModels();
44
48
  if (!Array.isArray(models)) return [];
@@ -67,16 +67,17 @@ export function createProviderReadiness({ rt, keychain, getReg, getWarmProviderM
67
67
  await awaitKeychainPrewarm();
68
68
  if (rt.closeRequested) throw new Error('runtime is closing');
69
69
  const initKey = providerInitCacheKey(providerConfig);
70
- const existing = providerInitPromises.get(initKey);
71
- if (existing) return await existing;
72
70
  // Provider initialization is idempotent for one normalized config. Keep
73
71
  // the fulfilled promise, not only the in-flight one: session resume used
74
72
  // to rerun registry/keychain setup on every click even though neither the
75
73
  // provider config nor the registry had changed. Auth/config mutations call
76
74
  // invalidateProviderCaches(), which clears this gate and preserves the
77
75
  // existing refresh semantics.
78
- const providerInitPromise = Promise.resolve().then(() => getReg().initProviders(providerConfig));
79
- providerInitPromises.set(initKey, providerInitPromise);
76
+ let providerInitPromise = providerInitPromises.get(initKey);
77
+ if (!providerInitPromise) {
78
+ providerInitPromise = Promise.resolve().then(() => getReg().initProviders(providerConfig));
79
+ providerInitPromises.set(initKey, providerInitPromise);
80
+ }
80
81
  let result;
81
82
  try {
82
83
  result = await providerInitPromise;
@@ -84,13 +85,18 @@ export function createProviderReadiness({ rt, keychain, getReg, getWarmProviderM
84
85
  if (providerInitPromises.get(initKey) === providerInitPromise) providerInitPromises.delete(initKey);
85
86
  throw error;
86
87
  }
87
- if (!rt.startupProviderCatalogRefreshStarted && !rt.closeRequested) {
88
- rt.startupProviderCatalogRefreshStarted = true;
89
- rt.startupProviderCatalogRefreshPending = true;
88
+ if (!rt.closeRequested) {
90
89
  try {
91
- void Promise.resolve(getReg().refreshProviderCatalogsOnStartup())
90
+ // The registry memoizes the refresh per catalog epoch; a provider set
91
+ // change yields a new promise, which gets its own completion handling.
92
+ const refresh = getReg().refreshProviderCatalogsOnStartup();
93
+ if (refresh === rt.startupProviderCatalogRefreshObserved) return result;
94
+ rt.startupProviderCatalogRefreshObserved = refresh;
95
+ rt.startupProviderCatalogRefreshPending = true;
96
+ const isLatest = () => rt.startupProviderCatalogRefreshObserved === refresh;
97
+ void Promise.resolve(refresh)
92
98
  .then(() => {
93
- rt.startupProviderCatalogRefreshPending = false;
99
+ if (isLatest()) rt.startupProviderCatalogRefreshPending = false;
94
100
  if (rt.closeRequested) return;
95
101
  // Fresh catalog rows invalidate model-derived caches, but the
96
102
  // already initialized provider registry remains valid.
@@ -103,7 +109,7 @@ export function createProviderReadiness({ rt, keychain, getReg, getWarmProviderM
103
109
  bootProfile('provider-catalogs:refresh-ready');
104
110
  })
105
111
  .catch((error) => {
106
- rt.startupProviderCatalogRefreshPending = false;
112
+ if (isLatest()) rt.startupProviderCatalogRefreshPending = false;
107
113
  bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
108
114
  });
109
115
  bootProfile('provider-catalogs:refresh-started');
@@ -28,11 +28,13 @@ export function createExtensionSync({
28
28
  }
29
29
  }
30
30
 
31
- // Apply per-server MCP changes in order; every other server stays untouched.
31
+ // Apply per-server MCP changes in order, then reconcile: a configured server
32
+ // added, renamed or removed can move a plugin server to another name, and
33
+ // only the reconcile connects it there and drops the stale name. Servers
34
+ // whose resolved config is unchanged stay untouched.
32
35
  async function applyMcpChanges(changes) {
33
- let status;
34
- for (const { name, enabled } of changes) status = await connectConfiguredMcp({ only: name, enabled });
35
- return status;
36
+ for (const { name, enabled } of changes) await connectConfiguredMcp({ only: name, enabled });
37
+ return connectConfiguredMcp({ reconcile: true });
36
38
  }
37
39
 
38
40
  async function refreshState(kind, detail = null) {
@@ -41,7 +43,11 @@ export function createExtensionSync({
41
43
  const mcpChanges = kind === 'mcp' ? detail?.mcpChanges : null;
42
44
  if (Array.isArray(mcpChanges)) {
43
45
  await applyMcpChanges(mcpChanges);
44
- } else if (kind === 'mcp' || kind === 'plugins') {
46
+ } else if (kind === 'plugins') {
47
+ // Plugin servers are derived from the registry: connect what is new or
48
+ // changed, drop what is gone or disabled, leave the rest connected.
49
+ await connectConfiguredMcp({ reconcile: true });
50
+ } else if (kind === 'mcp') {
45
51
  await connectConfiguredMcp({ reset: true });
46
52
  }
47
53
  await refreshSurface(kind);
@@ -101,12 +107,21 @@ export function createExtensionSync({
101
107
  return status;
102
108
  }
103
109
 
110
+ /** After a plugin registry change: reconcile this runtime's MCP connections
111
+ * with the derived server set first, then let peers do the same. */
112
+ async function reconcilePluginsAndAnnounce() {
113
+ const status = await connectConfiguredMcp({ reconcile: true });
114
+ await announce('plugins');
115
+ return status;
116
+ }
117
+
104
118
  return {
105
119
  refreshSurface,
106
120
  publish,
107
121
  scheduleMcpToggle,
108
122
  announce,
109
123
  reconnectMcpAndAnnounce,
124
+ reconcilePluginsAndAnnounce,
110
125
  dispose: () => subscription.unsubscribe(),
111
126
  };
112
127
  }