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.
- package/README.md +0 -17
- package/package.json +16 -8
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +43 -32
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
- package/src/runtime/channels/lib/worker-main.mjs +39 -58
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/http-wire.mjs +39 -4
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/json-metrics.mjs +94 -0
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +30 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +12 -2
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +61 -32
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +28 -4
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +9 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +15 -17
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +403 -170
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +28 -14
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
- package/src/tui/{engine → session}/session-api.mjs +17 -61
- package/src/tui/{engine → session}/session-flow.mjs +5 -3
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /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',
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
result
|
|
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
|
-
|
|
224
|
-
} catch (
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
539
|
-
//
|
|
540
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
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) {
|
|
@@ -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
|
|
4
|
-
// streaming
|
|
5
|
-
//
|
|
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 {
|
|
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
|
|
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
|
|
44
|
-
//
|
|
45
|
-
//
|
|
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) {
|