mixdog 0.9.44 → 0.9.46

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 (117) hide show
  1. package/package.json +5 -2
  2. package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
  3. package/scripts/agent-model-liveness-test.mjs +618 -0
  4. package/scripts/agent-trace-io-test.mjs +68 -4
  5. package/scripts/bench-run.mjs +30 -3
  6. package/scripts/compact-pressure-test.mjs +187 -4
  7. package/scripts/compact-prior-context-flatten-test.mjs +252 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +8 -6
  9. package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
  10. package/scripts/context-mcp-metering-test.mjs +75 -0
  11. package/scripts/explore-prompt-policy-test.mjs +15 -16
  12. package/scripts/explore-timeout-cancel-test.mjs +345 -0
  13. package/scripts/headless-pristine-execution-test.mjs +614 -0
  14. package/scripts/internal-comms-smoke.mjs +15 -6
  15. package/scripts/live-worker-smoke.mjs +1 -1
  16. package/scripts/memory-core-input-test.mjs +137 -0
  17. package/scripts/memory-rule-contract-test.mjs +5 -5
  18. package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
  19. package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
  20. package/scripts/parent-abort-link-test.mjs +22 -0
  21. package/scripts/provider-toolcall-test.mjs +22 -0
  22. package/scripts/reactive-compact-persist-smoke.mjs +8 -4
  23. package/scripts/session-bench.mjs +3 -70
  24. package/scripts/task-bench.mjs +0 -2
  25. package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
  26. package/scripts/tool-smoke.mjs +21 -21
  27. package/scripts/tool-tui-presentation-test.mjs +68 -0
  28. package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
  29. package/src/app.mjs +28 -103
  30. package/src/cli.mjs +17 -13
  31. package/src/headless-command.mjs +139 -0
  32. package/src/headless-role.mjs +121 -10
  33. package/src/help.mjs +4 -1
  34. package/src/rules/agent/00-common.md +3 -3
  35. package/src/rules/agent/00-core.md +8 -9
  36. package/src/rules/agent/20-skip-protocol.md +2 -3
  37. package/src/rules/agent/30-explorer.md +50 -56
  38. package/src/rules/agent/40-cycle1-agent.md +10 -12
  39. package/src/rules/agent/41-cycle2-agent.md +12 -9
  40. package/src/rules/agent/42-cycle3-agent.md +4 -6
  41. package/src/rules/lead/01-general.md +5 -6
  42. package/src/rules/lead/02-channels.md +1 -1
  43. package/src/rules/lead/lead-brief.md +14 -17
  44. package/src/rules/lead/lead-tool.md +3 -3
  45. package/src/rules/shared/01-tool.md +41 -43
  46. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
  48. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
  49. package/src/runtime/agent/orchestrator/config.mjs +96 -30
  50. package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  52. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
  53. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  54. package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
  55. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
  56. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
  58. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
  60. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
  61. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
  64. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
  65. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
  66. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
  67. package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
  69. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
  71. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
  74. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
  76. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
  77. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
  78. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
  80. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
  81. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  82. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
  84. package/src/runtime/memory/index.mjs +0 -1
  85. package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
  86. package/src/runtime/memory/lib/http-router.mjs +0 -193
  87. package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
  88. package/src/runtime/memory/tool-defs.mjs +5 -6
  89. package/src/runtime/shared/config.mjs +11 -34
  90. package/src/runtime/shared/pristine-execution-contract.json +75 -0
  91. package/src/runtime/shared/pristine-execution.mjs +356 -0
  92. package/src/runtime/shared/provider-api-key.mjs +43 -0
  93. package/src/runtime/shared/provider-auth-binding.mjs +21 -0
  94. package/src/runtime/shared/update-checker.mjs +40 -10
  95. package/src/session-runtime/context-status.mjs +61 -13
  96. package/src/session-runtime/mcp-glue.mjs +29 -2
  97. package/src/session-runtime/plugin-mcp.mjs +7 -0
  98. package/src/session-runtime/resource-api.mjs +38 -5
  99. package/src/session-runtime/runtime-core.mjs +5 -1
  100. package/src/session-runtime/session-turn-api.mjs +14 -2
  101. package/src/session-runtime/settings-api.mjs +5 -0
  102. package/src/session-runtime/tool-catalog.mjs +13 -2
  103. package/src/session-runtime/tool-defs.mjs +1 -3
  104. package/src/standalone/agent-task-status.mjs +50 -11
  105. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  106. package/src/standalone/explore-tool.mjs +257 -49
  107. package/src/standalone/seeds.mjs +1 -0
  108. package/src/tui/App.jsx +26 -16
  109. package/src/tui/app/use-transcript-scroll.mjs +4 -3
  110. package/src/tui/app/use-transcript-window.mjs +12 -21
  111. package/src/tui/components/ContextPanel.jsx +19 -25
  112. package/src/tui/dist/index.mjs +89 -78
  113. package/src/tui/engine/agent-envelope.mjs +16 -5
  114. package/src/tui/engine/labels.mjs +1 -1
  115. package/src/workflows/default/WORKFLOW.md +21 -51
  116. package/src/workflows/solo/WORKFLOW.md +12 -17
  117. package/src/workflows/solo-review/WORKFLOW.md +0 -47
@@ -7,8 +7,30 @@ import {
7
7
  summarizeContextMessages,
8
8
  toolSchemaSignature,
9
9
  } from '../runtime/agent/orchestrator/session/context-utils.mjs';
10
+ import { resolveWorkerCompactPolicy } from '../runtime/agent/orchestrator/session/loop/compact-policy.mjs';
10
11
  import { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
11
12
 
13
+ const DEFERRED_CATALOG_TOOL_PROVIDERS = new Set(['anthropic', 'anthropic-oauth']);
14
+
15
+ // Mirrors the tool-list portion of the Anthropic adapters without changing
16
+ // their wire serialization. Other native-deferred providers expose the
17
+ // catalog through BP1/system content, which is already metered there.
18
+ export function requestSerializedToolsForContext(session, provider) {
19
+ const activeTools = Array.isArray(session?.tools) ? session.tools : [];
20
+ const normalizedProvider = String(provider || '').trim().toLowerCase();
21
+ if (!DEFERRED_CATALOG_TOOL_PROVIDERS.has(normalizedProvider)
22
+ || session?.deferredNativeTools !== true
23
+ || activeTools.length === 0) {
24
+ return activeTools;
25
+ }
26
+ const activeNames = new Set(activeTools.map((tool) => String(tool?.name || '').trim()).filter(Boolean));
27
+ const catalog = Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : [];
28
+ const deferredTools = catalog
29
+ .filter((tool) => tool?.name && !activeNames.has(String(tool.name)))
30
+ .map((tool) => ({ ...tool, deferLoading: true }));
31
+ return deferredTools.length ? [...activeTools, ...deferredTools] : activeTools;
32
+ }
33
+
12
34
  // Live /context gauge computation + its self-owned memoization cache. Extracted
13
35
  // verbatim from the runtime API object; the runtime injects live getters for
14
36
  // the mutable session/route/cwd/mode locals. The cache (key + value) is owned
@@ -18,7 +40,15 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
18
40
  let contextStatusCacheKey = null;
19
41
  let contextStatusCacheValue = null;
20
42
 
21
- function contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature }) {
43
+ function contextStatusCacheKeyFor({
44
+ messages,
45
+ tools,
46
+ messagesRevision,
47
+ toolsSignature,
48
+ requestProvider,
49
+ requestToolCount,
50
+ requestToolsSignature,
51
+ }) {
22
52
  const session = getSession();
23
53
  const route = getRoute();
24
54
  const compaction = session?.compaction || {};
@@ -39,6 +69,9 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
39
69
  tools,
40
70
  toolCount: tools.length,
41
71
  toolsSignature,
72
+ requestProvider,
73
+ requestToolCount,
74
+ requestToolsSignature,
42
75
  contextWindow: session?.contextWindow || null,
43
76
  rawContextWindow: session?.rawContextWindow || null,
44
77
  effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
@@ -89,22 +122,33 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
89
122
  const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
90
123
  const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
91
124
  const tools = Array.isArray(session?.tools) ? session.tools : [];
125
+ const requestProvider = session?.provider || route.provider;
126
+ const requestTools = requestSerializedToolsForContext(session, requestProvider);
92
127
  const messagesRevision = contextMessagesRevision(messages);
93
128
  const toolsSignature = toolSchemaSignature(tools);
94
- const cacheKey = contextStatusCacheKeyFor({ messages, tools, messagesRevision, toolsSignature });
129
+ const requestToolsSignature = toolSchemaSignature(requestTools);
130
+ const cacheKey = contextStatusCacheKeyFor({
131
+ messages,
132
+ tools,
133
+ messagesRevision,
134
+ toolsSignature,
135
+ requestProvider,
136
+ requestToolCount: requestTools.length,
137
+ requestToolsSignature,
138
+ });
95
139
  if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
96
140
  return contextStatusCacheValue;
97
141
  }
98
142
 
99
143
  const messageSummary = summarizeContextMessages(messages);
100
- const toolSchemaTokens = estimateToolSchemaTokens(tools);
101
- const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
102
- const requestReserveTokens = estimateRequestReserveTokens(tools);
144
+ const toolSchemaTokens = estimateToolSchemaTokens(requestTools);
145
+ const toolSchemaBreakdown = estimateToolSchemaBreakdown(requestTools);
146
+ const requestReserveTokens = estimateRequestReserveTokens(requestTools);
103
147
  const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
104
148
  const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
105
149
  const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
106
150
  const lastContextTokens = Number(session?.lastContextTokens || 0);
107
- const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
151
+ const estimatedContextTokens = estimateTranscriptContextUsage(messages, requestTools, {
108
152
  messageCount: messageSummary.count,
109
153
  estimatedMessageTokens: messageSummary.estimatedTokens,
110
154
  });
@@ -125,15 +169,18 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
125
169
  // the gauge numerator.
126
170
  const usedTokens = estimatedContextTokens;
127
171
  const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
128
- // Use the same shared compact-policy math as manager/loop. Do not trust
129
- // persisted trigger telemetry as an independent policy input: it is an
130
- // output snapshot and was the source of repeated /context false positives.
131
- // Shared session-compaction policy (same math as manager/loop): agent-owned
132
- // semantic sessions report/fire at 90% of the boundary; main/user
133
- // recall-fasttrack report/fire on the boundary itself (100%).
134
- const compactPolicy = resolveSessionCompactPolicy(session || {}, compactBoundaryTokens);
172
+ // Use the worker policy when a boundary is available so target/reserve
173
+ // headroom, trigger, buffer tokens, and buffer ratio stay identical to the
174
+ // auto-compact decision. Fall back only for incomplete session metadata.
175
+ const workerCompactPolicy = resolveWorkerCompactPolicy(session, tools);
176
+ const compactPolicy = workerCompactPolicy?.boundaryTokens
177
+ ? workerCompactPolicy
178
+ : resolveSessionCompactPolicy(session || {}, compactBoundaryTokens);
135
179
  const compactTriggerTokens = compactPolicy.triggerTokens || 0;
136
180
  const compactBufferTokens = compactPolicy.bufferTokens || 0;
181
+ const compactBufferRatio = Number.isFinite(compactPolicy.bufferRatio)
182
+ ? compactPolicy.bufferRatio
183
+ : null;
137
184
  const value = {
138
185
  sessionId: session?.id || null,
139
186
  provider: route.provider,
@@ -155,6 +202,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
155
202
  boundaryTokens: compactBoundaryTokens || null,
156
203
  triggerTokens: compactTriggerTokens || null,
157
204
  bufferTokens: compactBufferTokens || null,
205
+ bufferRatio: compactBufferRatio,
158
206
  currentEstimatedTokens: estimatedContextTokens,
159
207
  lastApiRequestTokens: lastContextTokens || 0,
160
208
  lastApiRequestStale: lastUsageStale,
@@ -6,13 +6,19 @@
6
6
  import { resolve } from 'node:path';
7
7
  import { statSync } from 'node:fs';
8
8
  import { clean } from './session-text.mjs';
9
- import { readProjectMcpServers } from './plugin-mcp.mjs';
9
+ import { normalizeMcpProjectPathKey, readProjectMcpServers } from './plugin-mcp.mjs';
10
10
 
11
11
  // Cache project-local `.mcp.json` reads by path + mtime so repeated mcpStatus()
12
12
  // calls skip existsSync+readFileSync+JSON.parse when the file is unchanged.
13
13
  // Invalidated automatically on any mtime change (or create/delete via mtime=0).
14
14
  const projectMcpCache = new Map();
15
15
  const PROJECT_MCP_CACHE_MAX = 32;
16
+ export function invalidateProjectMcpCache(cwd) {
17
+ projectMcpCache.delete(resolve(cwd || '.', '.mcp.json'));
18
+ }
19
+ function mcpDisabled() {
20
+ return /^(?:1|true|on|yes)$/i.test(String(process.env.MIXDOG_DISABLE_MCP || ''));
21
+ }
16
22
  function cachedProjectMcpServers(cwd) {
17
23
  const path = resolve(cwd || '.', '.mcp.json');
18
24
  let mtimeMs = 0;
@@ -48,12 +54,23 @@ export function createMcpGlue({
48
54
  // (precedence: project > user config). `sources[name]` records each server's
49
55
  // origin ('config' | 'project') for status reporting.
50
56
  function resolveEffectiveMcpServers() {
57
+ // Benchmark/embedding guard: do not even inspect project `.mcp.json`.
58
+ if (mcpDisabled()) return { servers: {}, sources: {} };
51
59
  const config = getConfig();
52
60
  const configured = config?.mcpServers && typeof config.mcpServers === 'object'
53
61
  ? config.mcpServers
54
62
  : {};
63
+ const projectKey = normalizeMcpProjectPathKey(getCurrentCwd());
64
+ const overrides = config?.mcpProjectOverrides?.[projectKey];
65
+ const foldedConfigured = {};
66
+ for (const [name, cfg] of Object.entries(configured)) {
67
+ const override = overrides?.[name];
68
+ foldedConfigured[name] = typeof override?.enabled === 'boolean'
69
+ ? { ...cfg, enabled: override.enabled }
70
+ : cfg;
71
+ }
55
72
  const project = cachedProjectMcpServers(getCurrentCwd());
56
- const servers = { ...configured, ...project };
73
+ const servers = { ...foldedConfigured, ...project };
57
74
  const sources = {};
58
75
  for (const name of Object.keys(configured)) sources[name] = 'config';
59
76
  for (const name of Object.keys(project)) sources[name] = 'project';
@@ -61,6 +78,9 @@ export function createMcpGlue({
61
78
  }
62
79
 
63
80
  function mcpStatus() {
81
+ if (mcpDisabled()) {
82
+ return { servers: [], configuredCount: 0, connectedCount: 0, failedCount: 0 };
83
+ }
64
84
  const { servers: configured, sources } = resolveEffectiveMcpServers();
65
85
  const connected = new Map((mcpClient.getMcpServerStatus?.() || []).map((row) => [row.name, row]));
66
86
  const failures = new Map((state.mcpFailures || []).map((row) => [row.name, row]));
@@ -129,6 +149,13 @@ export function createMcpGlue({
129
149
  }
130
150
 
131
151
  async function connectConfiguredMcp({ reset = false, only = null, enabled = true } = {}) {
152
+ if (mcpDisabled()) {
153
+ ++state.mcpConnectGeneration;
154
+ state.mcpFailures = [];
155
+ if (only) await mcpClient.disconnectMcpServer?.(only);
156
+ else await mcpClient.disconnectAll?.();
157
+ return mcpStatus();
158
+ }
132
159
  // Scoped single-server toggle: non-superseding. It must NEVER cancel a
133
160
  // pending full {reset} (cwd-change/boot). So do not bump the generation;
134
161
  // just wait for any in-flight run, then bail if a newer full reset has
@@ -4,6 +4,13 @@ import { isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { clean } from './session-text.mjs';
5
5
  import { readJsonSafe } from './fs-utils.mjs';
6
6
 
7
+ // Config keys must compare the same cwd spelling on read and write. Windows
8
+ // paths are case-insensitive, so canonicalize their resolved form to lowercase.
9
+ export function normalizeMcpProjectPathKey(cwd) {
10
+ const path = resolve(cwd || '.');
11
+ return process.platform === 'win32' ? path.toLowerCase() : path;
12
+ }
13
+
7
14
  // Project-local MCP ingress: read `.mcp.json` from the project root and return
8
15
  // a cleaned { name: cfg } map. Best-effort — never throws. Accepts either the
9
16
  // standard `{ mcpServers: {...} }` shape or a bare name->cfg map. Self-ref
@@ -18,7 +18,9 @@ import {
18
18
  pluginMcpEnableScript,
19
19
  resolveContainedPluginPath,
20
20
  setProjectMcpServerEnabled,
21
+ normalizeMcpProjectPathKey,
21
22
  } from './plugin-mcp.mjs';
23
+ import { invalidateProjectMcpCache } from './mcp-glue.mjs';
22
24
 
23
25
  // MCP servers, skills, plugins, hooks, and memory/recall surfaces. Extracted
24
26
  // verbatim from the runtime API object; stateless helpers are imported directly
@@ -137,7 +139,19 @@ export function createResourceApi(deps) {
137
139
  throw new Error(`MCP server not configured: ${serverName}`);
138
140
  }
139
141
  delete current[serverName];
140
- saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
142
+ const currentOverrides = nextConfig.mcpProjectOverrides && typeof nextConfig.mcpProjectOverrides === 'object'
143
+ ? nextConfig.mcpProjectOverrides
144
+ : {};
145
+ const mcpProjectOverrides = {};
146
+ for (const [projectKey, serverOverrides] of Object.entries(currentOverrides)) {
147
+ if (!serverOverrides || typeof serverOverrides !== 'object' || Array.isArray(serverOverrides)) continue;
148
+ const nextServerOverrides = { ...serverOverrides };
149
+ delete nextServerOverrides[serverName];
150
+ if (Object.keys(nextServerOverrides).length > 0) {
151
+ mcpProjectOverrides[projectKey] = nextServerOverrides;
152
+ }
153
+ }
154
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: current, mcpProjectOverrides });
141
155
  const status = await connectConfiguredMcp({ reset: true });
142
156
  invalidatePreSessionToolSurface();
143
157
  const session = getSession();
@@ -152,11 +166,13 @@ export function createResourceApi(deps) {
152
166
  // A project-local `.mcp.json` entry WINS over config for this name, so the
153
167
  // durable toggle must land in whichever file actually drives the server.
154
168
  // For project-sourced servers, persist the `enabled` flag into `.mcp.json`
155
- // (the mtime bump invalidates the project cache), then run the same
169
+ // then explicitly invalidate the project cache (mtime granularity is not
170
+ // reliable for same-tick writes), before running the same
156
171
  // background connect/recreate chain used for config servers.
157
172
  const shadowRow = mcpStatus().servers.find((s) => s.name === serverName);
158
173
  if (shadowRow && shadowRow.source === 'project') {
159
174
  setProjectMcpServerEnabled(getCurrentCwd(), serverName, want);
175
+ invalidateProjectMcpCache(getCurrentCwd());
160
176
  return scheduleMcpToggle(serverName, want);
161
177
  }
162
178
  const nextConfig = { ...getConfig() };
@@ -166,12 +182,29 @@ export function createResourceApi(deps) {
166
182
  if (!Object.prototype.hasOwnProperty.call(current, serverName)) {
167
183
  throw new Error(`MCP server not configured: ${serverName}`);
168
184
  }
169
- // Adopt + persist config synchronously (fast) so intent is durable, then
185
+ // Keep the global server definition single-source; only this project's
186
+ // enabled override is adopted + persisted synchronously (fast), then
170
187
  // hand the heavy connect/close/recreate to the per-server background
171
188
  // chain. Return that chain's promise so callers can settle the picker on
172
189
  // completion, but the store no longer blocks on it.
173
- current[serverName] = { ...(current[serverName] || {}), enabled: want };
174
- saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
190
+ const projectKey = normalizeMcpProjectPathKey(getCurrentCwd());
191
+ const currentOverrides = nextConfig.mcpProjectOverrides && typeof nextConfig.mcpProjectOverrides === 'object'
192
+ ? nextConfig.mcpProjectOverrides
193
+ : {};
194
+ const projectOverrides = currentOverrides[projectKey] && typeof currentOverrides[projectKey] === 'object'
195
+ ? currentOverrides[projectKey]
196
+ : {};
197
+ cfgMod.markMcpProjectOverrideDirty(projectKey, serverName, want);
198
+ saveConfigAndAdopt({
199
+ ...nextConfig,
200
+ mcpProjectOverrides: {
201
+ ...currentOverrides,
202
+ [projectKey]: {
203
+ ...projectOverrides,
204
+ [serverName]: { enabled: want },
205
+ },
206
+ },
207
+ });
175
208
  return scheduleMcpToggle(serverName, want);
176
209
  },
177
210
  skillsStatus() {
@@ -1051,7 +1051,7 @@ export async function createMixdogSessionRuntime({
1051
1051
  const toolsStartedAt = performance.now();
1052
1052
  const standaloneTools = [
1053
1053
  TOOL_SEARCH_TOOL,
1054
- SKILL_TOOL,
1054
+ ...(envFlag('MIXDOG_DISABLE_SKILLS') ? [] : [SKILL_TOOL]),
1055
1055
  CWD_TOOL,
1056
1056
  SESSION_MANAGE_TOOL,
1057
1057
  EXPLORE_TOOL,
@@ -1141,6 +1141,10 @@ export async function createMixdogSessionRuntime({
1141
1141
  routingSessionId: callerSessionId,
1142
1142
  clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
1143
1143
  notifyFn: notifyFnForSession(callerSessionId),
1144
+ // Cascade caller cancellation (ESC / owner abort) into the explore
1145
+ // fan-out so every child dispatch tears down immediately — same
1146
+ // signal the adjacent native search already forwards.
1147
+ signal: callerCtx?.signal || session?.controller?.signal,
1144
1148
  });
1145
1149
  }
1146
1150
  if (name === 'cwd') {
@@ -11,6 +11,18 @@ import {
11
11
  } from './tool-catalog.mjs';
12
12
  import { getMcpTools } from '../runtime/agent/orchestrator/mcp/client.mjs';
13
13
 
14
+ export function splitToolStatusCounts(rows) {
15
+ const list = Array.isArray(rows) ? rows : [];
16
+ const regular = list.filter((row) => row?.kind !== 'mcp' && row?.kind !== 'skill');
17
+ const mcp = list.filter((row) => row?.kind === 'mcp');
18
+ return {
19
+ count: regular.length,
20
+ activeCount: regular.filter((row) => row.active).length,
21
+ mcpToolCount: mcp.length,
22
+ activeMcpToolCount: mcp.filter((row) => row.active).length,
23
+ };
24
+ }
25
+
14
26
  // Turn execution (ask) + session-manage/tool-surface/agent surfaces. Extracted
15
27
  // verbatim from the runtime API object; stateless helpers are imported directly
16
28
  // and the runtime injects live getters/setters for the mutable session/mode/
@@ -311,13 +323,13 @@ export function createSessionTurnApi(deps) {
311
323
  const activeNames = new Set((surface?.tools || []).map((tool) => tool?.name).filter(Boolean));
312
324
  const needle = clean(query).toLowerCase();
313
325
  const rows = catalog.map((tool) => toolRow(tool, activeNames)).filter((row) => row.name);
326
+ const counts = splitToolStatusCounts(rows);
314
327
  const tools = needle
315
328
  ? rows.filter((row) => toolSearchMatches(row, needle))
316
329
  : rows;
317
330
  return {
318
331
  mode: getMode(),
319
- count: rows.length,
320
- activeCount: rows.filter((row) => row.active).length,
332
+ ...counts,
321
333
  tools,
322
334
  activeTools: sortedNamesByMeasuredUsage(activeNames),
323
335
  discoveredTools: sortedNamesByMeasuredUsage(surface?.deferredDiscoveredTools || []),
@@ -189,6 +189,11 @@ export function createSettingsApi({
189
189
  next.type = compactType;
190
190
  next.compactType = compactType;
191
191
  }
192
+ // These controls apply only to main/user recall-fasttrack sessions;
193
+ // agent-owned semantic sessions retain their existing `buffer*` policy.
194
+ for (const key of ['mainBufferTokens', 'mainBuffer', 'mainBufferPercent', 'mainBufferPct', 'mainBufferRatio', 'mainBufferFraction']) {
195
+ if (hasOwn(input, key)) next[key] = input[key];
196
+ }
192
197
  const nextConfig = { ...config };
193
198
  nextConfig.compaction = normalizeCompactionConfig(next, { memoryEnabled: memoryEnabled() });
194
199
  saveConfigAndAdopt(nextConfig);
@@ -2,7 +2,7 @@
2
2
  // tool_search ranking + auto-selection, and the session tool-surface
3
3
  // application/selection logic. Pure module (session objects passed in).
4
4
  import { clean, LATE_TOOL_ANNOUNCEMENT_SENTINEL } from './session-text.mjs';
5
- import { estimateToolSchemaTokens } from '../runtime/agent/orchestrator/session/context-utils.mjs';
5
+ import { estimateToolSchemaTokens, toolSchemaSignature } from '../runtime/agent/orchestrator/session/context-utils.mjs';
6
6
  import { applyInitialDeferredToolManifestToBp1, buildDeferredToolManifest } from '../runtime/agent/orchestrator/context/collect.mjs';
7
7
  import { getMcpServerInstructionsMap } from '../runtime/agent/orchestrator/mcp/client.mjs';
8
8
  import {
@@ -40,7 +40,12 @@ function sameToolSchemaEntries(cached, tools) {
40
40
  || entry.inputSchema !== tool?.inputSchema
41
41
  || entry.input_schema !== tool?.input_schema
42
42
  || entry.parameters !== tool?.parameters
43
- || entry.schema !== tool?.schema) return false;
43
+ || entry.schema !== tool?.schema
44
+ || entry.deferLoading !== tool?.deferLoading
45
+ || entry.defer_loading !== tool?.defer_loading
46
+ || entry.annotationsMixdogKind !== tool?.annotations?.mixdogKind
47
+ || entry.annotationsAgentHidden !== tool?.annotations?.agentHidden
48
+ || entry.wireSignature !== toolSchemaSignature([tool])) return false;
44
49
  }
45
50
  return true;
46
51
  }
@@ -54,6 +59,11 @@ function toolSchemaEntry(tool) {
54
59
  input_schema: tool?.input_schema,
55
60
  parameters: tool?.parameters,
56
61
  schema: tool?.schema,
62
+ deferLoading: tool?.deferLoading,
63
+ defer_loading: tool?.defer_loading,
64
+ annotationsMixdogKind: tool?.annotations?.mixdogKind,
65
+ annotationsAgentHidden: tool?.annotations?.agentHidden,
66
+ wireSignature: toolSchemaSignature([tool]),
57
67
  };
58
68
  }
59
69
  export const DEFERRED_DEFAULT_FULL_TOOLS = Object.freeze([
@@ -143,6 +153,7 @@ export function toolSchemaBucket(tool) {
143
153
  const kind = toolKind(tool);
144
154
  if (kind === 'mcp') return 'mcp';
145
155
  if (kind === 'skill') return 'skills';
156
+ if (kind === 'control') return 'control';
146
157
  if (name === 'memory' || name === 'recall' || name.includes('memory')) return 'memory';
147
158
  if (name === 'search' || name === 'web_fetch') return 'web';
148
159
  if (['read', 'grep', 'find', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
@@ -13,13 +13,11 @@ export const TOOL_SEARCH_TOOL = {
13
13
  openWorldHint: false,
14
14
  agentHidden: true,
15
15
  },
16
- description: 'load_tool: pure loader for deferred tools. Pass names:["exact_tool_name", ...] (deferred tool names/aliases) to load them; returns loaded / already-active / missing plus any MCP servers still connecting (retry next turn) or failed. No keyword search, no ranking, no listing. Deferred tools can also be called directly by name (they auto-load on first call).',
16
+ description: 'Load deferred tools by exact name via names[]; reports loaded/already-active/missing plus MCP servers still connecting or failed. Deferred tools also auto-load when called directly.',
17
17
  inputSchema: {
18
18
  type: 'object',
19
19
  properties: {
20
20
  names: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Exact deferred tool names/aliases to load.' },
21
- select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Legacy alias for names (accepts "select:a,b").' },
22
- query: { type: 'string', description: 'Legacy: only "select:a,b" is honored (mapped to names); free-text keyword queries are rejected — this tool does not search.' },
23
21
  },
24
22
  additionalProperties: false,
25
23
  },
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  agentWatchdogPolicyActive,
3
3
  evaluateAgentWatchdogAbort,
4
+ resolveEffectiveToolRunningCeilingMs,
4
5
  } from '../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
5
6
 
6
7
  const ACTIVE_RUNTIME_STAGES = new Set([
@@ -18,10 +19,26 @@ function positiveSeconds(now, ts) {
18
19
  return Math.max(0, Math.floor((now - n) / 1000));
19
20
  }
20
21
 
21
- export function formatAgentWatchdogSummary(policy) {
22
+ export function formatAgentWatchdogSummary(policy, snapshot = null) {
22
23
  if (!policy || !agentWatchdogPolicyActive(policy)) return null;
24
+ const transportMs = policy.firstTransportMs ?? policy.firstResponseMs ?? 0;
25
+ const semanticMs = policy.firstSemanticMs ?? policy.firstVisibleCeilingMs ?? 0;
26
+ if (snapshot) {
27
+ if (snapshot.waitingForTransport && transportMs > 0) {
28
+ return `armed transport=${Math.round(transportMs / 1000)}s`;
29
+ }
30
+ if ((snapshot.waitingForFirstSemantic ?? snapshot.waitingForFirstActivity) && semanticMs > 0) {
31
+ return `armed semantic=${Math.round(semanticMs / 1000)}s`;
32
+ }
33
+ if (snapshot.stage === 'tool_running' && policy.toolRunningMs > 0) {
34
+ const effectiveMs = resolveEffectiveToolRunningCeilingMs(snapshot, policy);
35
+ return `armed tool=${Math.round(effectiveMs / 1000)}s`;
36
+ }
37
+ if (policy.idleStaleMs > 0) return `armed idle=${Math.round(policy.idleStaleMs / 1000)}s`;
38
+ }
23
39
  const parts = [];
24
- if (policy.firstResponseMs > 0) parts.push(`first=${Math.round(policy.firstResponseMs / 1000)}s`);
40
+ if (transportMs > 0) parts.push(`transport=${Math.round(transportMs / 1000)}s`);
41
+ if (semanticMs > 0) parts.push(`semantic=${Math.round(semanticMs / 1000)}s`);
25
42
  if (policy.idleStaleMs > 0) parts.push(`idle=${Math.round(policy.idleStaleMs / 1000)}s`);
26
43
  if (policy.toolRunningMs > 0) parts.push(`tool=${Math.round(policy.toolRunningMs / 1000)}s`);
27
44
  return parts.length ? `armed ${parts.join(' ')}` : null;
@@ -55,7 +72,7 @@ export function buildAgentTaskProgressFields({
55
72
  const stage = cleanStage(runtimeStage || snapshot?.stage || sessionStatus || 'unknown');
56
73
  const workerStage = stage;
57
74
  const silentFor = resolveSilentForSeconds(now, snapshot, runtime);
58
- const watchdog = formatAgentWatchdogSummary(policy);
75
+ const watchdog = formatAgentWatchdogSummary(policy, snapshot);
59
76
  const queued = Number.isFinite(Number(queuedFollowups)) && Number(queuedFollowups) > 0
60
77
  ? Math.floor(Number(queuedFollowups))
61
78
  : null;
@@ -96,13 +113,24 @@ function cleanStage(value) {
96
113
  }
97
114
 
98
115
  function describeLastProgress({ stage, snapshot, runtime, silentFor, lastToolCall, now }) {
99
- if (snapshot?.waitingForFirstActivity) return 'awaiting first model response';
116
+ if (snapshot?.waitingForTransport) return 'awaiting model transport';
117
+ if (snapshot?.waitingForFirstSemantic ?? snapshot?.waitingForFirstActivity) {
118
+ return 'transport active; awaiting first model event';
119
+ }
100
120
  if (stage === 'tool_running') {
101
121
  const tool = cleanStage(lastToolCall);
102
122
  return tool && tool !== 'unknown' ? `tool: ${tool}` : 'tool running';
103
123
  }
104
124
  if (stage === 'connecting' || stage === 'requesting') return 'connecting to model';
105
125
  if (stage === 'streaming') {
126
+ if (snapshot?.lastSemanticKind === 'tool') return 'tool protocol progress';
127
+ if (snapshot?.lastSemanticKind === 'text') return 'visible model text';
128
+ if (snapshot?.lastSemanticKind === 'reasoning') {
129
+ return snapshot?.lastVisibleTextAt
130
+ ? 'model reasoning (hidden; visible output previously emitted)'
131
+ : 'model reasoning (hidden; no visible output yet)';
132
+ }
133
+ if (snapshot?.hasFirstSemantic) return 'model active (no visible output yet)';
106
134
  const streamSilent = positiveSeconds(
107
135
  now,
108
136
  snapshot?.lastStreamDeltaAt || runtime?.lastStreamDeltaAt || 0,
@@ -149,9 +177,25 @@ function describeAgentDiagnostic({
149
177
  return `idle, ${queued} follow-up${queued === 1 ? '' : 's'} queued`;
150
178
  }
151
179
 
152
- if (snapshot?.waitingForFirstActivity) return 'waiting for first response';
180
+ if (policy) {
181
+ const abortErr = evaluateAgentWatchdogAbort(snapshot, now, policy);
182
+ if (abortErr) return `stale: ${abortErr.message.replace(/^agent /i, '')}`;
183
+ }
184
+
185
+ if (snapshot?.waitingForTransport) return 'waiting for model transport';
186
+ if (snapshot?.waitingForFirstSemantic ?? snapshot?.waitingForFirstActivity) {
187
+ return 'transport healthy; waiting for first semantic model event';
188
+ }
153
189
 
154
190
  if (stage === 'streaming') {
191
+ if (snapshot?.lastSemanticKind === 'tool') return 'tool protocol active';
192
+ if (snapshot?.lastSemanticKind === 'text') return 'visible text streaming';
193
+ if (snapshot?.lastSemanticKind === 'reasoning') {
194
+ return snapshot?.lastVisibleTextAt
195
+ ? 'hidden reasoning active; visible output previously emitted'
196
+ : 'hidden reasoning active; no visible output yet';
197
+ }
198
+ if (snapshot?.hasFirstSemantic) return 'model active; no visible output yet';
155
199
  const streamSilent = positiveSeconds(
156
200
  now,
157
201
  snapshot?.lastStreamDeltaAt || 0,
@@ -171,12 +215,7 @@ function describeAgentDiagnostic({
171
215
  now,
172
216
  policy,
173
217
  );
174
- if (abortErr) {
175
- return `stale: ${abortErr.message.replace(/^agent /i, '')}`;
176
- }
177
- if (silentFor >= 30) {
178
- return `stale: no stream/tool progress for ${silentFor}s`;
179
- }
218
+ if (abortErr) return `stale: ${abortErr.message.replace(/^agent /i, '')}`;
180
219
  }
181
220
 
182
221
  if (normalizedTask === 'running' && (stage === 'idle' || stage === 'done')) {
@@ -35,7 +35,7 @@ export const AGENT_TOOL = {
35
35
  openWorldHint: true,
36
36
  agentHidden: true,
37
37
  },
38
- description: 'Delegate scoped work. Agent handoffs always start background tasks and return task IDs immediately. Spawn every independent scope in parallel in the same turn, each with a distinct tag; for the same scope, use send or spawn with the same tag to reuse the live session. Wait for the completion notification before dependent work. Do not call status/read after spawn; they are manual recovery only.',
38
+ description: 'Delegate scoped work; handoffs always start background tasks (task ids return immediately). Distinct tags for independent scopes; spawn/send with the same tag reuses the live session for the same scope. Wait for the completion notification; do not call status/read after spawn (manual recovery only).',
39
39
  inputSchema: {
40
40
  type: 'object',
41
41
  properties: {