mixdog 0.9.96 → 0.9.99

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 (193) hide show
  1. package/README.md +0 -17
  2. package/package.json +16 -8
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/agent/30-explorer.md +43 -32
  9. package/src/rules/lead/01-general.md +1 -2
  10. package/src/rules/lead/lead-tool.md +0 -3
  11. package/src/rules/shared/01-tool.md +11 -10
  12. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  13. package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  15. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
  17. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
  18. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  19. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
  20. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  22. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  24. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  25. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  26. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
  27. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  28. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
  29. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  30. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
  31. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  32. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
  33. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  34. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  35. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  36. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  37. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  38. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  39. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  41. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
  42. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  43. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  44. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
  45. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  46. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  47. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  48. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  49. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  50. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  51. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  52. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
  55. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  58. package/src/runtime/attachments/pdf-extract.mjs +83 -0
  59. package/src/runtime/attachments/store.mjs +515 -0
  60. package/src/runtime/attachments/store.test.mjs +188 -0
  61. package/src/runtime/channels/lib/config.mjs +33 -33
  62. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  63. package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
  64. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  65. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  66. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  67. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  68. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  69. package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
  70. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  72. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  73. package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
  74. package/src/runtime/channels/lib/worker-main.mjs +39 -58
  75. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  76. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  77. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  78. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  79. package/src/runtime/channels/tool-defs.mjs +1 -1
  80. package/src/runtime/memory/index.mjs +72 -211
  81. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  82. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  83. package/src/runtime/memory/lib/http-router.mjs +1 -6
  84. package/src/runtime/memory/lib/http-wire.mjs +39 -4
  85. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  86. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  87. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  88. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  89. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  90. package/src/runtime/memory/lib/memory.mjs +1 -1
  91. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  92. package/src/runtime/search/index.mjs +2 -2
  93. package/src/runtime/shared/abort-race.mjs +70 -0
  94. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  95. package/src/runtime/shared/config.mjs +11 -24
  96. package/src/runtime/shared/json-metrics.mjs +94 -0
  97. package/src/runtime/shared/llm/index.mjs +1 -1
  98. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  99. package/src/runtime/shared/resource-admission.mjs +6 -5
  100. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  101. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  102. package/src/runtime/shared/tool-surface.mjs +91 -22
  103. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  104. package/src/runtime/shared/turn-snapshot.mjs +30 -2
  105. package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
  106. package/src/session-runtime/channel-config-api.mjs +5 -5
  107. package/src/session-runtime/config-lifecycle.mjs +27 -27
  108. package/src/session-runtime/lifecycle-api.mjs +22 -3
  109. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  110. package/src/session-runtime/prewarm.mjs +1 -1
  111. package/src/session-runtime/provider-models.mjs +12 -2
  112. package/src/session-runtime/quick-model-rows.mjs +38 -7
  113. package/src/session-runtime/quick-search-models.mjs +15 -17
  114. package/src/session-runtime/remote-control.mjs +6 -7
  115. package/src/session-runtime/runtime-core.mjs +61 -32
  116. package/src/session-runtime/runtime-tunables.mjs +9 -6
  117. package/src/session-runtime/self-update.mjs +1 -1
  118. package/src/session-runtime/session-lifecycle.mjs +36 -15
  119. package/src/session-runtime/session-title.mjs +28 -4
  120. package/src/session-runtime/session-turn-api.mjs +55 -15
  121. package/src/session-runtime/settings-api.mjs +7 -7
  122. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  123. package/src/standalone/channel-admin.mjs +42 -53
  124. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  125. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
  126. package/src/standalone/channel-worker.mjs +124 -647
  127. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
  128. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  129. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  130. package/src/standalone/session-protocol.mjs +9 -4
  131. package/src/standalone/session-runtime-pool.mjs +393 -0
  132. package/src/standalone/session-runtime-record.mjs +20 -0
  133. package/src/standalone/session-runtime-worker.mjs +203 -0
  134. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
  135. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
  136. package/src/standalone/session-wire.mjs +55 -0
  137. package/src/tui/App.jsx +4 -4
  138. package/src/tui/app/app-format.mjs +1 -1
  139. package/src/tui/app/channel-pickers.mjs +41 -41
  140. package/src/tui/app/input-parsers.mjs +2 -2
  141. package/src/tui/app/onboarding-steps.mjs +2 -2
  142. package/src/tui/app/project-picker.mjs +8 -8
  143. package/src/tui/app/prompt-submit.mjs +15 -17
  144. package/src/tui/app/settings-picker.mjs +25 -25
  145. package/src/tui/app/use-prompt-handlers.mjs +7 -3
  146. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  147. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  148. package/src/tui/components/ToolExecution.jsx +1 -1
  149. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  150. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  151. package/src/tui/dist/index.mjs +403 -170
  152. package/src/tui/hooks/useSession.mjs +16 -0
  153. package/src/tui/index.jsx +11 -11
  154. package/src/tui/paste-attachments.mjs +13 -7
  155. package/src/tui/prompt-history-store.mjs +52 -4
  156. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  157. package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
  158. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  159. package/src/tui/{engine → session}/context-state.mjs +3 -3
  160. package/src/tui/{engine → session}/labels.mjs +2 -2
  161. package/src/tui/{engine → session}/live-share.mjs +28 -14
  162. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  163. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  164. package/src/tui/session/oauth-flows.mjs +140 -0
  165. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  166. package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
  167. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  168. package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
  169. package/src/tui/{engine → session}/session-api.mjs +17 -61
  170. package/src/tui/{engine → session}/session-flow.mjs +5 -3
  171. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  172. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  173. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  174. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  175. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  176. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  177. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  178. package/src/tui/{engine → session}/turn.mjs +29 -107
  179. package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
  180. package/src/tui/session.mjs +22 -0
  181. package/src/ui/statusline-segments.mjs +1 -1
  182. package/src/ui/statusline.mjs +1 -1
  183. package/vendor/ink/build/components/App.js +10 -1
  184. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  185. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  186. package/src/tui/engine/turn-watchdog.mjs +0 -92
  187. package/src/tui/engine.mjs +0 -22
  188. package/src/tui/hooks/useEngine.mjs +0 -16
  189. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  190. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  191. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  192. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  193. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -9,6 +9,8 @@ import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../.
9
9
  import { makeToolEnvelope, normalizeToolEnvelope } from '../session/tool-envelope.mjs';
10
10
  import { classifyResultKind } from '../session/result-classification.mjs';
11
11
  import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
12
+ import { createOwnerFairGate } from '../../../shared/owner-fair-gate.mjs';
13
+ import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.mjs';
12
14
  // --- Types ---
13
15
  /** Known auto-detect targets: port file path relative to tmpdir.
14
16
  * Note: `mixdog` used to self-loopback via active-instance.json's
@@ -17,7 +19,7 @@ import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
17
19
  * in-process through agent's toolExecutor (see orchestrator/internal-tools),
18
20
  * so this registry is for genuinely external port-based MCP targets only. */
19
21
  const AUTO_DETECT_PORTS = {
20
- 'mixdog-memory': { discovery: 'memory', dir: 'mixdog', file: 'active-instance.json', portField: 'memory_port', endpoint: '/mcp' },
22
+ 'mixdog-memory': { discovery: 'memory', endpoint: '/mcp' },
21
23
  };
22
24
  const DEFAULT_MCP_CALL_TIMEOUT_MS = 120000;
23
25
  // Per-server STARTUP handshake budget (connect + listTools): 10s.
@@ -25,6 +27,7 @@ const DEFAULT_MCP_STARTUP_TIMEOUT_MS = 10000;
25
27
  // --- State ---
26
28
  const servers = new Map();
27
29
  const reconnects = createKeyedSingleflight();
30
+ const callAdmissions = new Map();
28
31
  let mcpSdkPromise = null;
29
32
  // Memo for mcpToolHasField(name, field) — keyed by `${toolName}|${field}`.
30
33
  // The lookup (regex parse + servers Map get + tools.find + schema property
@@ -165,6 +168,45 @@ export function getMcpServerStatus() {
165
168
  })(),
166
169
  }));
167
170
  }
171
+ function positiveInt(value, fallback) {
172
+ const parsed = Math.floor(Number(value));
173
+ return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
174
+ }
175
+ function callAdmissionFor(server) {
176
+ let gate = callAdmissions.get(server.name);
177
+ if (gate) return gate;
178
+ const cfg = server?.cfg || {};
179
+ gate = createOwnerFairGate({
180
+ name: `MCP ${server.name}`,
181
+ activeMax: positiveInt(
182
+ cfg.maxConcurrency ?? cfg.max_concurrency ?? process.env.MIXDOG_MCP_MAX_INFLIGHT,
183
+ 8,
184
+ ),
185
+ queueMax: positiveInt(
186
+ cfg.maxQueue ?? cfg.max_queue ?? process.env.MIXDOG_MCP_MAX_QUEUE,
187
+ 256,
188
+ ),
189
+ minOwnerQueue: 8,
190
+ waitTimeoutMs: positiveInt(
191
+ cfg.waitTimeoutMs ?? cfg.wait_timeout_ms ?? process.env.MIXDOG_MCP_WAIT_TIMEOUT_MS,
192
+ 30_000,
193
+ ),
194
+ });
195
+ callAdmissions.set(server.name, gate);
196
+ return gate;
197
+ }
198
+ function closeCallAdmission(name, reason) {
199
+ const gate = callAdmissions.get(name);
200
+ if (!gate) return;
201
+ callAdmissions.delete(name);
202
+ gate.close(reason || `MCP ${name} disconnected`);
203
+ }
204
+ export function getMcpAdmissionSnapshot() {
205
+ return [...callAdmissions.entries()].map(([name, gate]) => ({
206
+ name,
207
+ ...gate.snapshot(),
208
+ }));
209
+ }
168
210
 
169
211
  /** Snapshot of MCP initialize `instructions` per connected server (handshake time). */
170
212
  export function getMcpServerInstructionsMap() {
@@ -199,7 +241,7 @@ async function reconnectMcpServer(serverName, failedServer) {
199
241
  * Execute an MCP tool call.
200
242
  * Name format: `mcp__{serverName}__{toolName}`
201
243
  */
202
- export async function executeMcpTool(name, args) {
244
+ export async function executeMcpTool(name, args, options = {}) {
203
245
  // Parse: mcp__{server}__{tool}
204
246
  const match = name.match(/^mcp__(.+?)__(.+)$/);
205
247
  if (!match)
@@ -208,35 +250,40 @@ export async function executeMcpTool(name, args) {
208
250
  const server = servers.get(serverName);
209
251
  if (!server)
210
252
  throw new Error(`MCP server "${serverName}" not connected`);
211
- let result;
212
- try {
213
- result = await _callToolWithTimeout(server, toolName, args);
214
- } catch (firstErr) {
215
- const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
216
- if (isMcpToolCallTimeoutError(firstErr)) {
217
- mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
218
- throw firstErr;
219
- }
220
- mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
221
- let retryServer;
253
+ const gate = callAdmissionFor(server);
254
+ return gate.run(options?.ownerKey || currentToolExecutionOwner(), async () => {
255
+ let result;
222
256
  try {
223
- retryServer = await reconnectMcpServer(serverName, server);
224
- } catch (reconnectErr) {
225
- const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
226
- throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
227
- }
228
- try {
229
- result = await _callToolWithTimeout(retryServer, toolName, args);
230
- } catch (retryErr) {
231
- const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
232
- throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
257
+ result = await _callToolWithTimeout(server, toolName, args);
258
+ } catch (firstErr) {
259
+ const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
260
+ if (isMcpToolCallTimeoutError(firstErr)) {
261
+ mcpLog(`[mcp-client] Tool call timed out; skipping reconnect retry for "${serverName}/${toolName}".\n`);
262
+ throw firstErr;
263
+ }
264
+ mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
265
+ let retryServer;
266
+ try {
267
+ retryServer = await reconnectMcpServer(serverName, server);
268
+ } catch (reconnectErr) {
269
+ const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
270
+ throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
271
+ }
272
+ try {
273
+ result = await _callToolWithTimeout(retryServer, toolName, args);
274
+ } catch (retryErr) {
275
+ const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
276
+ throw new Error(`Tool call failed: ${firstMsg}; retry after reconnect also failed: ${retryMsg}`);
277
+ }
233
278
  }
234
- }
235
- const normalized = normalizeToolEnvelope(normalizeMcpToolResult(result));
236
- const text = capMcpOutput(normalized.result);
237
- return normalized.explicitSuccess
238
- ? makeToolEnvelope(text, [], { explicitSuccess: true })
239
- : text;
279
+ const normalized = normalizeToolEnvelope(normalizeMcpToolResult(result));
280
+ const text = capMcpOutput(normalized.result);
281
+ return normalized.explicitSuccess
282
+ ? makeToolEnvelope(text, [], { explicitSuccess: true })
283
+ : text;
284
+ }, {
285
+ signal: options?.signal || null,
286
+ });
240
287
  }
241
288
 
242
289
  // Preserve MCP failure metadata across the object→string boundary. The
@@ -425,6 +472,7 @@ export async function disconnectAll() {
425
472
  }
426
473
  catch { /* ignore */ }
427
474
  servers.delete(name);
475
+ closeCallAdmission(name, `MCP ${name} disconnected`);
428
476
  }
429
477
  _invalidateMcpToolFieldMemo();
430
478
  }
@@ -442,6 +490,7 @@ export async function disconnectMcpServer(name) {
442
490
  }
443
491
  catch { /* ignore */ }
444
492
  servers.delete(name);
493
+ closeCallAdmission(name, `MCP ${name} disconnected`);
445
494
  _invalidateMcpToolFieldMemo();
446
495
  return true;
447
496
  }
@@ -494,13 +543,11 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
494
543
  const spec = AUTO_DETECT_PORTS[cfg.autoDetect];
495
544
  if (!spec)
496
545
  throw new Error(`Unknown autoDetect target: "${cfg.autoDetect}"`);
497
- // Prefer the single-writer discovery advert (discovery/<service>.json),
498
- // pid-validated. Fall back to the legacy active-instance.json portField
499
- // when no live advert is present (cross-version compat).
546
+ // Read the pid-validated single-writer discovery advert.
500
547
  let port = spec.discovery ? readServicePort(spec.discovery, { requirePid: false }) : null;
501
548
  if (port && spec.discovery) _autoDetectAdvert = { service: spec.discovery, port };
502
549
  let portFile = null;
503
- if (!port) {
550
+ if (!port && spec.file) {
504
551
  portFile = spec.dir === 'mixdog' && process.env.MIXDOG_RUNTIME_ROOT
505
552
  ? join(process.env.MIXDOG_RUNTIME_ROOT, spec.file)
506
553
  : join(tmpdir(), spec.dir, spec.file);
@@ -525,6 +572,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
525
572
  port = parseInt(raw, 10);
526
573
  }
527
574
  }
575
+ if (!port) throw new Error(`autoDetect server "${name}": live service advert missing`);
528
576
  if (!Number.isFinite(port) || port < 1 || port > 65535) {
529
577
  throw new Error(`autoDetect server "${name}": invalid port value${portFile ? ` in ${portFile}` : ''}`);
530
578
  }
@@ -112,6 +112,13 @@ function _sanitizeGrokModels(models) {
112
112
  const display = _displayGrokModel(m);
113
113
  const family = _normalizeGrokFamily(m?.id);
114
114
  const reasoningLevels = _grokModelSupportsEffort(m?.id) ? ['none', 'low', 'medium', 'high'] : [];
115
+ // api.x.ai /models does not expose a max-output field. Historical
116
+ // enrichment could persist contextWindow as outputTokens (for example
117
+ // 500k/500k); clear it rather than advertise an invented request cap.
118
+ if (out?.outputTokens != null) {
119
+ changed = true;
120
+ out = { ...out, outputTokens: null };
121
+ }
115
122
  if (display && display !== out?.display) {
116
123
  changed = true;
117
124
  out = { ...out, display, name: out?.name || out?.id };
@@ -1,4 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import {
3
+ attachmentTextForPart,
4
+ isAttachmentReference,
5
+ readAttachmentBase64,
6
+ } from '../../../attachments/store.mjs';
2
7
 
3
8
  const DEFAULT_IMAGE_MIME = 'image/png';
4
9
 
@@ -9,6 +14,9 @@ function cleanMimeType(value) {
9
14
 
10
15
  function imageInfo(block) {
11
16
  if (!block || typeof block !== 'object' || block.type !== 'image') return null;
17
+ if (isAttachmentReference(block)) {
18
+ return { data: readAttachmentBase64(block), mimeType: cleanMimeType(block.mimeType || block.mediaType) };
19
+ }
12
20
  if (typeof block.data === 'string' && block.data) {
13
21
  return { data: block.data, mimeType: cleanMimeType(block.mimeType || block.mediaType) };
14
22
  }
@@ -32,10 +40,13 @@ function geminiInlineInfo(block) {
32
40
  // as their native document shape; estimators must never serialize its base64.
33
41
  function fileInfo(block) {
34
42
  if (!block || typeof block !== 'object' || block.type !== 'file') return null;
35
- if (typeof block.data !== 'string' || !block.data) return null;
43
+ const data = isAttachmentReference(block)
44
+ ? readAttachmentBase64(block)
45
+ : (typeof block.data === 'string' ? block.data : '');
46
+ if (!data) return null;
36
47
  const mimeType = String(block.mimeType || block.mediaType || 'application/pdf').trim().toLowerCase() || 'application/pdf';
37
48
  const filename = typeof block.filename === 'string' && block.filename ? block.filename : '';
38
- return { data: block.data, mimeType, filename };
49
+ return { data, mimeType, filename };
39
50
  }
40
51
 
41
52
  function imageUrlFromPart(block) {
@@ -95,6 +106,7 @@ function imageMimeFromDataUrl(url) {
95
106
  function textFromPart(block) {
96
107
  if (typeof block === 'string') return block;
97
108
  if (!block || typeof block !== 'object') return '';
109
+ if (block.type === 'text' && isAttachmentReference(block)) return attachmentTextForPart(block);
98
110
  if (typeof block.text === 'string') return block.text;
99
111
  if (typeof block.content === 'string') return block.content;
100
112
  return '';
@@ -112,7 +124,7 @@ function contentParts(content) {
112
124
  if (content && typeof content === 'object'
113
125
  && (imageUrlFromPart(content) || imageFileIdFromPart(content)
114
126
  || imageFileUriFromPart(content) || geminiInlineInfo(content)
115
- || fileInfo(content))) return [content];
127
+ || fileInfo(content) || isAttachmentReference(content))) return [content];
116
128
  return null;
117
129
  }
118
130
 
@@ -149,6 +161,14 @@ function imageIdentity(kind, value, mimeType = '') {
149
161
 
150
162
  function imageDescriptor(part) {
151
163
  if (!part || typeof part !== 'object') return null;
164
+ if (part.type === 'image' && isAttachmentReference(part)) {
165
+ return {
166
+ identity: imageIdentity('attachment-ref', part.attachmentRef, part.mimeType || part.mediaType),
167
+ width: positiveDimension(part.width, part.dimensions?.width),
168
+ height: positiveDimension(part.height, part.dimensions?.height),
169
+ detail: String(part.detail || 'auto').toLowerCase(),
170
+ };
171
+ }
152
172
  const info = imageInfo(part);
153
173
  const inline = geminiInlineInfo(part);
154
174
  const url = imageUrlFromPart(part);
@@ -187,6 +207,9 @@ export function contentFileDescriptors(content) {
187
207
  const parts = contentParts(content);
188
208
  if (!parts) return [];
189
209
  return parts.flatMap((part) => {
210
+ if (part?.type === 'file' && isAttachmentReference(part)) {
211
+ return [{ mimeType: String(part.mimeType || part.mediaType || 'application/pdf'), sizeBytes: Number(part.sizeBytes) || 0 }];
212
+ }
190
213
  const file = fileInfo(part);
191
214
  if (!file) return [];
192
215
  return [{ mimeType: file.mimeType, sizeBytes: Math.floor((file.data.length * 3) / 4) }];
@@ -215,6 +238,7 @@ function storedHistoryImagePlaceholder(part) {
215
238
  function sanitizePartForStoredHistory(part) {
216
239
  if (typeof part === 'string') return part;
217
240
  if (!part || typeof part !== 'object') return part;
241
+ if (isAttachmentReference(part)) return part;
218
242
  if (part.type === 'image' || part.type === 'image_url' || part.type === 'input_image' || imageUrlFromPart(part) || imageFileIdFromPart(part) || imageFileUriFromPart(part) || geminiInlineInfo(part)) {
219
243
  return { type: 'text', text: storedHistoryImagePlaceholder(part) };
220
244
  }
@@ -307,6 +331,9 @@ export function normalizeContentForAnthropic(content) {
307
331
  if (part?.type === 'input_text' || part?.type === 'output_text') {
308
332
  return { type: 'text', text: part.text || '' };
309
333
  }
334
+ if (part?.type === 'text' && isAttachmentReference(part)) {
335
+ return { type: 'text', text: attachmentTextForPart(part) };
336
+ }
310
337
  return part;
311
338
  });
312
339
  }
@@ -360,7 +360,7 @@ function _modelsDevMetadataSync(id, provider) {
360
360
  if (!pid) return null;
361
361
  if (!_mdCache) {
362
362
  warmModelsDevFromDiskSync();
363
- if (!_mdCache) { void loadModelsDevCatalog(); return null; }
363
+ if (!_mdCache) return null;
364
364
  }
365
365
  const row = _mdCache?.[pid]?.models?.[id];
366
366
  if (!row || !row.cost) return null;
@@ -372,10 +372,12 @@ function _modelsDevMetadataSync(id, provider) {
372
372
  // require a `cost` field (staleness only needs family/release_date) and
373
373
  // returns the raw row untouched. Warms from disk one-shot if memory is cold;
374
374
  // returns null when the catalog is unavailable so callers can skip filtering.
375
+ // Sync reads never start network work: session warmup owns remote catalog I/O,
376
+ // and injected/provider-local transports must remain request-hermetic.
375
377
  // `_test` (tests only) injects a fake catalog map without touching disk.
376
378
  export function getModelsDevRowSync(id, provider, _test) {
377
379
  const cat = _test || _mdCache || (warmModelsDevFromDiskSync(), _mdCache);
378
- if (!cat) { void loadModelsDevCatalog(); return null; }
380
+ if (!cat) return null;
379
381
  const pid = _modelsDevProviderId(provider);
380
382
  if (!pid) return null;
381
383
  const row = cat?.[pid]?.models?.[id];
@@ -387,7 +389,7 @@ export function getModelsDevRowSync(id, provider, _test) {
387
389
  // provider's whole catalog. Returns null when the catalog is cold/unavailable.
388
390
  export function getModelsDevProviderModelsSync(provider, _test) {
389
391
  const cat = _test || _mdCache || (warmModelsDevFromDiskSync(), _mdCache);
390
- if (!cat) { void loadModelsDevCatalog(); return null; }
392
+ if (!cat) return null;
391
393
  const pid = _modelsDevProviderId(provider);
392
394
  if (!pid) return null;
393
395
  const models = cat?.[pid]?.models;
@@ -535,12 +535,9 @@ export async function _streamResponse({
535
535
  // only) trips a short, named terminal StreamStalledError instead of coasting
536
536
  // to the 300s inter-chunk cap / 30-min agent watchdog.
537
537
  let semanticIdleTimer = null;
538
- // WS gets its own shorter semantic-idle window. The shared SSE semantic
539
- // timeout is intentionally near the 300s stall warning to protect Anthropic
540
- // effort-mode "silent thinking"; using that value here lets a wedged WS
541
- // request race the TUI 300s watchdog. Keep websocket stalls provider-owned:
542
- // fail/retry/fallback here first, then let the TUI watchdog remain a final
543
- // safety net only.
538
+ // WS owns its semantic-idle window. The TUI backstop is deliberately much
539
+ // longer, so this timeout can fail/retry/fallback at the provider boundary
540
+ // without racing an outer turn abort (Claude Code/Codex parity).
544
541
  const semanticIdleMs = PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS;
545
542
  const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
546
543
  // First-meaningful-frame watchdog timer + one-shot latch. Armed alongside
@@ -31,6 +31,22 @@ function readModelsFromCacheFile(fileName) {
31
31
  }
32
32
  }
33
33
 
34
+ export function providerCachedModelsSync(provider) {
35
+ const p = String(provider || '').toLowerCase();
36
+ const files = PROVIDER_CACHE_FILES[p] || [];
37
+ const rows = [];
38
+ const seen = new Set();
39
+ for (const fileName of files) {
40
+ for (const row of readModelsFromCacheFile(fileName)) {
41
+ const id = rowId(row);
42
+ if (!id || seen.has(id)) continue;
43
+ seen.add(id);
44
+ rows.push({ ...row, id });
45
+ }
46
+ }
47
+ return rows;
48
+ }
49
+
34
50
  function modelAliases(id) {
35
51
  const text = String(id || '').trim();
36
52
  if (!text) return [];
@@ -60,11 +76,17 @@ function getProviderCachedModelSync(provider, model) {
60
76
  }
61
77
 
62
78
  export function providerCachedModelMetadataSync(provider, model) {
79
+ const p = String(provider || '').toLowerCase();
63
80
  const row = getProviderCachedModelSync(provider, model);
64
81
  if (!row) return null;
65
82
  return {
66
83
  contextWindow: num(row.contextWindow ?? row.context_window ?? row.maxContextWindow ?? row.max_context_window ?? row.max_input_tokens),
67
- outputTokens: num(row.outputTokens ?? row.maxOutputTokens ?? row.max_output_tokens ?? row.output_token_limit),
84
+ // Grok's /models response currently publishes a context window but no
85
+ // output ceiling. Older enriched cache rows copied contextWindow into
86
+ // outputTokens; never feed that derived value back as provider-native.
87
+ outputTokens: p === 'grok-oauth'
88
+ ? null
89
+ : num(row.outputTokens ?? row.maxOutputTokens ?? row.max_output_tokens ?? row.output_token_limit),
68
90
  supportsVision: row.supportsVision === true,
69
91
  supportsFunctionCalling: row.supportsFunctionCalling === true,
70
92
  supportsWebSearch: row.supportsWebSearch === true,
@@ -35,7 +35,7 @@ let _initChain = Promise.resolve();
35
35
  let _inFlightPromise = null;
36
36
  let _inFlightSig = null;
37
37
  let _lastAppliedSig = null;
38
- // Provider instances are process-global inside the backend daemon. Catalog
38
+ // Provider instances are process-global inside the daemon. Catalog
39
39
  // readers use this revision to share one raw model snapshot while still
40
40
  // rebuilding their cheap route/config projection after auth/config changes.
41
41
  let _providerCatalogRevision = 0;
@@ -367,7 +367,7 @@ function warmupCatalogs() {
367
367
  }
368
368
  }
369
369
 
370
- // Force-refresh each provider's /models catalog ONCE per backend-daemon
370
+ // Force-refresh each provider's /models catalog once per daemon.
371
371
  // lifetime. Every session runtime joins this process-global promise and then
372
372
  // reads the same provider-instance caches. Unlike
373
373
  // warmupCatalogs (which calls listModels() and so respects the 24h provider
@@ -30,7 +30,7 @@ import {
30
30
  } from './loop/tool-classify.mjs';
31
31
  import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
32
32
  import { runRecallFastTrackCompact } from './loop/recall-fasttrack.mjs';
33
- import { executeTool, _scopedCacheOutcomeForCall } from './loop/tool-exec.mjs';
33
+ import { executeTool, _scopedCacheOutcomeForCall, resolveLiveToolCwd } from './loop/tool-exec.mjs';
34
34
 
35
35
  // classifyResultKind is imported from result-classification.mjs at the top of
36
36
  // this file; import it from there directly rather than via this module.
@@ -420,11 +420,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
420
420
  const name = String(call?.name || call?.toolName || call?.function?.name || '').toLowerCase();
421
421
  return name === 'sleep' || name.endsWith('/sleep') || name.endsWith('.sleep');
422
422
  };
423
- // Tool execution must use the session cwd even when the caller omitted the
424
- // legacy positional cwd argument. Agent workers always carry their cwd on
425
- // sessionRef; falling through to pwd()/process.cwd() resolves relatives
426
- // against the host/plugin root instead of the worker workspace.
427
- cwd = cwd || sessionRef?.cwd || undefined;
423
+ // sessionRef.cwd is the live SSOT. The legacy positional cwd is only the
424
+ // turn-start snapshot and becomes stale after an in-turn cwd tool call.
425
+ cwd = resolveLiveToolCwd(cwd, sessionRef);
428
426
  // Staged pre-cap warnings + one true hard stop. The ONLY count-based
429
427
  // forced termination is the hard cap at maxLoopIterations (default 200):
430
428
  // a genuine runaway guard. Before it, staged warnings fire at 50%/75%/90%
@@ -445,6 +443,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
445
443
  Math.floor(maxLoopIterations * 0.9),
446
444
  ];
447
445
  while (true) {
446
+ // A cwd tool call updates sessionRef in place. Refresh before building
447
+ // this iteration's eager dispatcher and cache keys so every following
448
+ // tool family, including apply_patch, observes the new write root.
449
+ cwd = resolveLiveToolCwd(cwd, sessionRef);
448
450
  const _iterT0 = Date.now();
449
451
  throwIfAborted();
450
452
  if (iterations >= maxLoopIterations) {
@@ -53,4 +53,4 @@ export {
53
53
  export {
54
54
  semanticCompactMessages,
55
55
  recallFastTrackCompactMessages,
56
- } from './compact/engine.mjs';
56
+ } from './compact/runner.mjs';
@@ -1,11 +1,19 @@
1
1
  // Eager tool-dispatch controller, extracted from agent-loop.mjs. Owns the
2
2
  // per-turn pending promise map, the intra-turn in-flight signature set, and
3
- // the mutation epoch. Every valid call may start while the provider is still
4
- // streaming, including apply_patch and shell in the same batch. Tool-local
5
- // conflict guards own safety; results are collected later in call order.
3
+ // the mutation epoch. Every valid call dispatches while the provider is still
4
+ // streaming. Calls execute in parallel except that shell after apply_patch
5
+ // waits for every earlier patch in the turn; results are collected later in
6
+ // call order.
6
7
  import { normalizeToolEnvelope } from './tool-envelope.mjs';
7
8
  import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
8
- import { _intraTurnSig, _isReadTool, _isScopedCacheableTool, _stripMcpPrefix } from './loop/tool-classify.mjs';
9
+ import {
10
+ _intraTurnSig,
11
+ _isMutationTool,
12
+ _isReadTool,
13
+ _isScopedCacheableTool,
14
+ _isShellTool,
15
+ _stripMcpPrefix,
16
+ } from './loop/tool-classify.mjs';
9
17
  import { tryReadCached, tryScopedToolCached } from './read-dedup.mjs';
10
18
  import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
11
19
  import { executeTool } from './loop/tool-exec.mjs';
@@ -18,6 +26,10 @@ export function createEagerDispatcher({
18
26
  executeToolFn = executeTool,
19
27
  }) {
20
28
  const pending = new Map();
29
+ // Cumulative settlement barrier for patches already emitted in this
30
+ // assistant turn. Patches remain path-parallel with each other; only a
31
+ // later shell waits, so validation cannot observe pre-patch files.
32
+ let patchBarrier = Promise.resolve();
21
33
  // Streaming-time intra-turn dedup. When the LLM emits two
22
34
  // tool_use blocks with identical (name, args) signatures in
23
35
  // sequence, the provider's onToolCall fires for both BEFORE
@@ -90,9 +102,11 @@ export function createEagerDispatcher({
90
102
  // this call there, never start it eagerly here.
91
103
  if (preDispatchDenyForSession(sessionRef, call, toolKind) !== null) return null;
92
104
  const entry = { startedAt: Date.now(), endedAt: null, mutationEpoch: epoch.mutation };
105
+ const precedingPatches = _isShellTool(call.name) ? patchBarrier : null;
93
106
  if (_dedupEligible) _eagerInFlightSigs.set(_sig, call.id);
94
107
  entry.promise = (async () => {
95
108
  try {
109
+ if (precedingPatches) await precedingPatches;
96
110
  await opts.beforeToolExecution?.();
97
111
  return { ok: true, value: await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: getNextIteration(), deferShellCwdCommit: true }) };
98
112
  } catch (error) {
@@ -152,6 +166,10 @@ export function createEagerDispatcher({
152
166
  return settled;
153
167
  });
154
168
  pending.set(call.id, entry);
169
+ if (_isMutationTool(call.name)) {
170
+ const currentPatch = entry.promise;
171
+ patchBarrier = Promise.allSettled([patchBarrier, currentPatch]).then(() => undefined);
172
+ }
155
173
  return entry;
156
174
  };
157
175
  const startEagerRun = (calls, startIndex, dupSet) => {
@@ -23,6 +23,7 @@ import {
23
23
  } from './tool-helpers.mjs';
24
24
  import { isOnDeferredToolSurface, prepareDeferredToolCallThrough } from './deferred-call-through.mjs';
25
25
  import { preDispatchDenyForSession, routeWebFetchCall } from './pre-dispatch-deny.mjs';
26
+ import { runWithToolExecutionOwner } from '../../../../shared/tool-execution-owner.mjs';
26
27
 
27
28
  let codeGraphRuntimePromise = null;
28
29
  async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
@@ -32,6 +33,11 @@ async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options
32
33
  return mod.executeCodeGraphTool(name, args, cwd, signal, options);
33
34
  }
34
35
 
36
+ export function resolveLiveToolCwd(cwd, sessionRef) {
37
+ const liveCwd = typeof sessionRef?.cwd === 'string' ? sessionRef.cwd : '';
38
+ return liveCwd || cwd;
39
+ }
40
+
35
41
  export function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
36
42
  if (executeOpts.scopedCacheOutcome) {
37
43
  if (sessionRef && toolCallId) {
@@ -51,7 +57,16 @@ export function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, cal
51
57
  return outcome;
52
58
  }
53
59
 
54
- export async function executeTool(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
60
+ export function executeTool(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
61
+ return runWithToolExecutionOwner(callerSessionId, () =>
62
+ executeToolOwned(name, args, cwd, callerSessionId, sessionRef, executeOpts));
63
+ }
64
+
65
+ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
66
+ // cwd is captured when the turn starts. The deferred cwd tool updates
67
+ // sessionRef.cwd in place, so every later tool call must re-read that live
68
+ // value instead of continuing to use the stale turn snapshot.
69
+ cwd = resolveLiveToolCwd(cwd, sessionRef);
55
70
  const scopedCacheOutcome = _scopedCacheOutcomeForCall(
56
71
  sessionRef,
57
72
  executeOpts.toolCallId,
@@ -174,7 +189,10 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
174
189
  && mcpToolHasField(name, 'cwd')
175
190
  && (args == null || args.cwd == null);
176
191
  const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
177
- return executeMcpTool(name, finalArgs);
192
+ return executeMcpTool(name, finalArgs, {
193
+ signal: executeOpts.signal || null,
194
+ ownerKey: callerSessionId,
195
+ });
178
196
  }
179
197
  if (name === 'code_graph') {
180
198
  // cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
@@ -40,9 +40,10 @@ export function isEagerDispatchable(name, tools) {
40
40
  return set.has(name);
41
41
  }
42
42
 
43
- // Full-parallel dispatch policy: EVERY valid tool call in an assistant turn
44
- // starts immediately. Mutations rely on their own conflict scope (apply_patch
45
- // uses per-path locks), so disjoint writes never wait behind a global barrier.
43
+ // Full dispatch policy: EVERY valid tool call in an assistant turn is queued
44
+ // immediately. Mutations rely on their own conflict scope (apply_patch uses
45
+ // per-path locks); the eager dispatcher adds only the apply_patch -> shell
46
+ // execution barrier required for same-turn validation.
46
47
  // isEagerDispatchable above remains the READ-ONLY classifier used for dedup
47
48
  // eligibility, steering, and post-mutation result invalidation.
48
49
  export function isParallelDispatchable(name) {