mixdog 0.9.95 → 0.9.96
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/package.json +12 -9
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/shared/01-tool.md +20 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +26 -11
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +118 -18
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +87 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +29 -1
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +10 -10
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +2 -2
- package/src/runtime/channels/lib/inbound-handler.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/memory/index.mjs +5 -38
- package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +8 -0
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +60 -31
- package/src/runtime/shared/config.mjs +130 -44
- package/src/runtime/shared/resource-admission.mjs +146 -97
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-surface.mjs +0 -5
- package/src/runtime/shared/turn-snapshot.mjs +31 -5
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/runtime-core.mjs +11 -11
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/session-lifecycle.mjs +9 -2
- package/src/session-runtime/session-turn-api.mjs +41 -9
- package/src/session-runtime/settings-api.mjs +5 -17
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/backend-daemon.mjs +97 -36
- package/src/standalone/channel-admin.mjs +12 -12
- package/src/standalone/channel-daemon-client.mjs +17 -1
- package/src/standalone/channel-daemon-transport.mjs +90 -8
- package/src/standalone/engine-daemon-client.mjs +380 -205
- package/src/standalone/engine-daemon-protocol.mjs +3 -4
- package/src/standalone/engine-daemon-service.mjs +378 -134
- package/src/standalone/engine-daemon-transport.mjs +268 -77
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/session-protocol.mjs +188 -0
- package/src/tui/App.jsx +30 -17
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +1 -2
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +46 -41
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +6 -8
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +9 -2
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/dist/index.mjs +920 -1592
- package/src/tui/engine/live-share.mjs +34 -0
- package/src/tui/engine/session-api-ext.mjs +1 -4
- package/src/tui/engine/session-api.mjs +103 -33
- package/src/tui/engine/session-flow.mjs +7 -1
- package/src/tui/engine/tool-card-results.mjs +4 -2
- package/src/tui/engine/tool-result-text.mjs +10 -0
- package/src/tui/engine/turn.mjs +9 -69
- package/src/tui/engine-local-session.mjs +47 -31
- package/src/tui/index.jsx +1 -2
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { availableParallelism } from 'node:os';
|
|
2
|
+
import { Worker } from 'node:worker_threads';
|
|
3
|
+
import { currentProviderAdmissionOwner } from './admission-scheduler.mjs';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MIN_BATCH_BYTES = 32 * 1024;
|
|
6
|
+
|
|
7
|
+
function positiveInt(value, fallback) {
|
|
8
|
+
const parsed = Math.floor(Number(value));
|
|
9
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function configuredWorkerCount(env = process.env) {
|
|
13
|
+
const configured = Number(env.MIXDOG_PROVIDER_STREAM_WORKERS);
|
|
14
|
+
if (configured === 0) return 0;
|
|
15
|
+
if (Number.isFinite(configured) && configured > 0) return Math.floor(configured);
|
|
16
|
+
return Math.max(1, Math.min(4, availableParallelism() - 1));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function abortError(signal) {
|
|
20
|
+
return signal?.reason instanceof Error
|
|
21
|
+
? signal.reason
|
|
22
|
+
: new Error(String(signal?.reason || 'provider stream parse canceled'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function syntaxError(details) {
|
|
26
|
+
const error = new SyntaxError(String(details?.message || 'invalid JSON'));
|
|
27
|
+
error.name = String(details?.name || 'SyntaxError');
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reusable worker pool for the CPU part of provider SSE handling.
|
|
33
|
+
*
|
|
34
|
+
* Small deltas stay inline because a Worker round-trip costs more than parsing
|
|
35
|
+
* them. Large events and multi-event network chunks are parsed off the daemon
|
|
36
|
+
* event loop. Calls are never admission-capped: every request is posted
|
|
37
|
+
* immediately and workers consume their independent message queues.
|
|
38
|
+
*/
|
|
39
|
+
export function createStreamJsonPool({
|
|
40
|
+
maxWorkers = configuredWorkerCount(),
|
|
41
|
+
minBatchBytes = Number(process.env.MIXDOG_PROVIDER_STREAM_WORKER_MIN_BYTES)
|
|
42
|
+
|| DEFAULT_MIN_BATCH_BYTES,
|
|
43
|
+
maxPendingBytes = (Number(process.env.MIXDOG_PROVIDER_STREAM_PENDING_MB) || 32)
|
|
44
|
+
* 1024 * 1024,
|
|
45
|
+
WorkerImpl = Worker,
|
|
46
|
+
} = {}) {
|
|
47
|
+
const workerMax = Math.max(0, Math.floor(Number(maxWorkers) || 0));
|
|
48
|
+
const inlineBelowBytes = Math.max(0, Math.floor(Number(minBatchBytes) || 0));
|
|
49
|
+
const slots = [];
|
|
50
|
+
const ownerAffinities = new Map();
|
|
51
|
+
const waiting = [];
|
|
52
|
+
const pendingByteMax = Math.max(1024 * 1024, Math.floor(Number(maxPendingBytes) || 0));
|
|
53
|
+
let pendingBytes = 0;
|
|
54
|
+
let waitingBytes = 0;
|
|
55
|
+
let sequence = 0;
|
|
56
|
+
let closed = false;
|
|
57
|
+
const stats = {
|
|
58
|
+
inlineBatches: 0,
|
|
59
|
+
offloadedBatches: 0,
|
|
60
|
+
fallbackBatches: 0,
|
|
61
|
+
parsedBytes: 0,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function parseInline(payloads) {
|
|
65
|
+
return payloads.map((payload) => JSON.parse(payload));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function removeSlot(slot) {
|
|
69
|
+
const index = slots.indexOf(slot);
|
|
70
|
+
if (index >= 0) slots.splice(index, 1);
|
|
71
|
+
for (const [owner, assigned] of ownerAffinities) {
|
|
72
|
+
if (assigned === slot) ownerAffinities.delete(owner);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function failSlot(slot) {
|
|
77
|
+
if (slot.failed) return;
|
|
78
|
+
slot.failed = true;
|
|
79
|
+
removeSlot(slot);
|
|
80
|
+
for (const task of slot.tasks.values()) {
|
|
81
|
+
pendingBytes = Math.max(0, pendingBytes - task.bytes);
|
|
82
|
+
task.detach();
|
|
83
|
+
if (task.aborted) continue;
|
|
84
|
+
stats.fallbackBatches += 1;
|
|
85
|
+
try { task.resolve(parseInline(task.payloads)); }
|
|
86
|
+
catch (error) { task.reject(error); }
|
|
87
|
+
}
|
|
88
|
+
slot.tasks.clear();
|
|
89
|
+
try { slot.worker.terminate(); } catch {}
|
|
90
|
+
drainWaiting();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createSlot() {
|
|
94
|
+
const worker = new WorkerImpl(new URL('./stream-json-worker.mjs', import.meta.url), {
|
|
95
|
+
execArgv: [],
|
|
96
|
+
});
|
|
97
|
+
worker.unref?.();
|
|
98
|
+
const slot = { worker, tasks: new Map(), failed: false };
|
|
99
|
+
worker.on('message', (message) => {
|
|
100
|
+
const task = slot.tasks.get(Number(message?.id));
|
|
101
|
+
if (!task) return;
|
|
102
|
+
slot.tasks.delete(Number(message.id));
|
|
103
|
+
pendingBytes = Math.max(0, pendingBytes - task.bytes);
|
|
104
|
+
task.detach();
|
|
105
|
+
if (task.aborted) return;
|
|
106
|
+
if (message?.ok === true) task.resolve(message.values);
|
|
107
|
+
else task.reject(syntaxError(message?.error));
|
|
108
|
+
drainWaiting();
|
|
109
|
+
});
|
|
110
|
+
worker.on('error', () => failSlot(slot));
|
|
111
|
+
worker.on('exit', () => {
|
|
112
|
+
if (!closed) failSlot(slot);
|
|
113
|
+
else removeSlot(slot);
|
|
114
|
+
});
|
|
115
|
+
slots.push(slot);
|
|
116
|
+
return slot;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pickSlot(ownerKey) {
|
|
120
|
+
const ready = slots.filter((slot) => !slot.failed);
|
|
121
|
+
const owner = String(ownerKey || '').trim().slice(0, 240);
|
|
122
|
+
const assigned = owner ? ownerAffinities.get(owner) : null;
|
|
123
|
+
if (assigned && !assigned.failed) return assigned;
|
|
124
|
+
const least = ready.sort((left, right) => left.tasks.size - right.tasks.size)[0] || null;
|
|
125
|
+
const slot = ready.length < workerMax && (!least || least.tasks.size > 0)
|
|
126
|
+
? createSlot()
|
|
127
|
+
: least || createSlot();
|
|
128
|
+
if (owner) {
|
|
129
|
+
ownerAffinities.set(owner, slot);
|
|
130
|
+
while (ownerAffinities.size > 4096) {
|
|
131
|
+
ownerAffinities.delete(ownerAffinities.keys().next().value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return slot;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function removeWaiting(task) {
|
|
138
|
+
const index = waiting.indexOf(task);
|
|
139
|
+
if (index < 0) return false;
|
|
140
|
+
waiting.splice(index, 1);
|
|
141
|
+
waitingBytes = Math.max(0, waitingBytes - task.bytes);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function postTask(task) {
|
|
146
|
+
const slot = pickSlot(task.ownerKey);
|
|
147
|
+
task.slot = slot;
|
|
148
|
+
slot.tasks.set(task.id, task);
|
|
149
|
+
pendingBytes += task.bytes;
|
|
150
|
+
try {
|
|
151
|
+
slot.worker.postMessage({ id: task.id, payloads: task.payloads });
|
|
152
|
+
} catch {
|
|
153
|
+
slot.tasks.delete(task.id);
|
|
154
|
+
pendingBytes = Math.max(0, pendingBytes - task.bytes);
|
|
155
|
+
task.detach();
|
|
156
|
+
stats.fallbackBatches += 1;
|
|
157
|
+
try { task.resolve(parseInline(task.payloads)); }
|
|
158
|
+
catch (error) { task.reject(error); }
|
|
159
|
+
drainWaiting();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function drainWaiting() {
|
|
164
|
+
while (waiting.length > 0) {
|
|
165
|
+
const task = waiting[0];
|
|
166
|
+
if (pendingBytes > 0 && pendingBytes + task.bytes > pendingByteMax) return;
|
|
167
|
+
waiting.shift();
|
|
168
|
+
waitingBytes = Math.max(0, waitingBytes - task.bytes);
|
|
169
|
+
if (task.aborted) continue;
|
|
170
|
+
postTask(task);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseBatch(values, {
|
|
175
|
+
signal = null,
|
|
176
|
+
ownerKey = currentProviderAdmissionOwner(),
|
|
177
|
+
} = {}) {
|
|
178
|
+
const payloads = (Array.isArray(values) ? values : [values]).map((value) => String(value));
|
|
179
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
180
|
+
const bytes = payloads.reduce((sum, payload) => sum + Buffer.byteLength(payload), 0);
|
|
181
|
+
stats.parsedBytes += bytes;
|
|
182
|
+
if (workerMax === 0 || bytes < inlineBelowBytes) {
|
|
183
|
+
stats.inlineBatches += 1;
|
|
184
|
+
try { return Promise.resolve(parseInline(payloads)); }
|
|
185
|
+
catch (error) { return Promise.reject(error); }
|
|
186
|
+
}
|
|
187
|
+
if (closed) return Promise.reject(new Error('provider stream JSON pool is closed'));
|
|
188
|
+
if (bytes > pendingByteMax) {
|
|
189
|
+
const error = new Error(
|
|
190
|
+
`provider stream JSON batch exceeded ${pendingByteMax} bytes`,
|
|
191
|
+
);
|
|
192
|
+
error.code = 'ERESOURCEPRESSURE';
|
|
193
|
+
return Promise.reject(error);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const id = ++sequence;
|
|
197
|
+
stats.offloadedBatches += 1;
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const task = {
|
|
200
|
+
id,
|
|
201
|
+
ownerKey,
|
|
202
|
+
bytes,
|
|
203
|
+
payloads,
|
|
204
|
+
resolve,
|
|
205
|
+
reject,
|
|
206
|
+
aborted: false,
|
|
207
|
+
onAbort: null,
|
|
208
|
+
detach() {
|
|
209
|
+
if (!task.onAbort || !signal) return;
|
|
210
|
+
try { signal.removeEventListener('abort', task.onAbort); } catch {}
|
|
211
|
+
task.onAbort = null;
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
if (signal) {
|
|
215
|
+
task.onAbort = () => {
|
|
216
|
+
if (task.aborted) return;
|
|
217
|
+
task.aborted = true;
|
|
218
|
+
task.detach();
|
|
219
|
+
if (removeWaiting(task)) {
|
|
220
|
+
reject(abortError(signal));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
reject(abortError(signal));
|
|
224
|
+
};
|
|
225
|
+
signal.addEventListener('abort', task.onAbort, { once: true });
|
|
226
|
+
}
|
|
227
|
+
if (pendingBytes > 0 && pendingBytes + bytes > pendingByteMax) {
|
|
228
|
+
if (waitingBytes + bytes > pendingByteMax) {
|
|
229
|
+
task.detach();
|
|
230
|
+
const error = new Error(
|
|
231
|
+
`provider stream JSON backlog exceeded ${pendingByteMax} bytes`,
|
|
232
|
+
);
|
|
233
|
+
error.code = 'ERESOURCEPRESSURE';
|
|
234
|
+
reject(error);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
waiting.push(task);
|
|
238
|
+
waitingBytes += bytes;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
postTask(task);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function close(reason = 'provider stream JSON pool closed') {
|
|
246
|
+
if (closed) return;
|
|
247
|
+
closed = true;
|
|
248
|
+
const error = new Error(reason);
|
|
249
|
+
for (const task of waiting.splice(0)) {
|
|
250
|
+
task.detach();
|
|
251
|
+
if (!task.aborted) task.reject(error);
|
|
252
|
+
}
|
|
253
|
+
waitingBytes = 0;
|
|
254
|
+
const workers = [];
|
|
255
|
+
for (const slot of slots.splice(0)) {
|
|
256
|
+
for (const task of slot.tasks.values()) {
|
|
257
|
+
task.detach();
|
|
258
|
+
if (!task.aborted) task.reject(error);
|
|
259
|
+
}
|
|
260
|
+
slot.tasks.clear();
|
|
261
|
+
workers.push(Promise.resolve(slot.worker.terminate()).catch(() => {}));
|
|
262
|
+
}
|
|
263
|
+
ownerAffinities.clear();
|
|
264
|
+
await Promise.all(workers);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function snapshot() {
|
|
268
|
+
return {
|
|
269
|
+
workers: slots.filter((slot) => !slot.failed).length,
|
|
270
|
+
workerMax,
|
|
271
|
+
activeBatches: slots.reduce((sum, slot) => sum + slot.tasks.size, 0),
|
|
272
|
+
pendingBytes,
|
|
273
|
+
waitingBatches: waiting.length,
|
|
274
|
+
waitingBytes,
|
|
275
|
+
maxPendingBytes: pendingByteMax,
|
|
276
|
+
ownerAffinities: ownerAffinities.size,
|
|
277
|
+
inlineBatches: stats.inlineBatches,
|
|
278
|
+
offloadedBatches: stats.offloadedBatches,
|
|
279
|
+
fallbackBatches: stats.fallbackBatches,
|
|
280
|
+
parsedBytes: stats.parsedBytes,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { parseBatch, close, snapshot };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export const providerStreamJsonPool = createStreamJsonPool();
|
|
288
|
+
export const parseProviderJsonBatch = (payloads, options) =>
|
|
289
|
+
providerStreamJsonPool.parseBatch(payloads, options);
|
|
290
|
+
export const providerStreamJsonSnapshot = () => providerStreamJsonPool.snapshot();
|
|
291
|
+
export const closeProviderStreamJsonPool = (reason) => providerStreamJsonPool.close(reason);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
if (!parentPort) throw new Error('provider stream JSON worker requires parentPort');
|
|
4
|
+
|
|
5
|
+
parentPort.on('message', (message) => {
|
|
6
|
+
const id = Number(message?.id);
|
|
7
|
+
const payloads = Array.isArray(message?.payloads) ? message.payloads : [];
|
|
8
|
+
try {
|
|
9
|
+
const values = payloads.map((payload) => JSON.parse(String(payload)));
|
|
10
|
+
parentPort.postMessage({ id, ok: true, values });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
parentPort.postMessage({
|
|
13
|
+
id,
|
|
14
|
+
ok: false,
|
|
15
|
+
error: {
|
|
16
|
+
name: String(error?.name || 'SyntaxError'),
|
|
17
|
+
message: String(error?.message || error || 'invalid JSON'),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
@@ -403,13 +403,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
403
403
|
// Continuations since the last executed tool batch — bounds the text-only
|
|
404
404
|
// continuation runaway (see PROVIDER_CONTINUATION_NO_TOOL_LIMIT).
|
|
405
405
|
let _continuationsSinceToolBatch = 0;
|
|
406
|
-
// Loop-level transport replays consumed
|
|
407
|
-
// TRANSPORT_RETRY_MAX)
|
|
406
|
+
// Loop-level transport replays consumed since the last SUCCESSFUL send
|
|
407
|
+
// (see send-with-recovery TRANSPORT_RETRY_MAX). Reference parity: codex
|
|
408
|
+
// starts each sampling request at retries=0 (and resets on transport
|
|
409
|
+
// fallback), cc's withRetry budget is per request, opencode schedules
|
|
410
|
+
// retries per stream call. A per-ask budget instead let one early blip in
|
|
411
|
+
// a long turn leave every later iteration with zero replays.
|
|
408
412
|
let _transportRetriesUsed = 0;
|
|
409
413
|
// Queued prompt/task notifications are attached after a
|
|
410
414
|
// tool batch, before the continuation provider send. Normal batches drain
|
|
411
415
|
// up to 'next'; a Sleep-like tool grants a 'later' flush.
|
|
412
416
|
let _toolBatchJustCompleted = false;
|
|
417
|
+
let _lastToolBatchEndedAt = 0;
|
|
413
418
|
let _lastToolBatchHadSleep = false;
|
|
414
419
|
const isSleepLikeToolCall = (call) => {
|
|
415
420
|
const name = String(call?.name || call?.toolName || call?.function?.name || '').toLowerCase();
|
|
@@ -457,7 +462,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
457
462
|
// non-empty final so callers never see an empty response.
|
|
458
463
|
if (response && !String(response.content || '').trim()) {
|
|
459
464
|
response.content = sessionAgent === 'explorer'
|
|
460
|
-
? 'EXPLORATION_FAILED'
|
|
465
|
+
? 'EXPLORATION_FAILED [iteration-cap] — explorer hit its tool-turn cap with zero anchors; retry once with changed tokens or a narrower root'
|
|
461
466
|
: '[iteration cap reached before final text]';
|
|
462
467
|
if (Array.isArray(response.toolCalls)) response.toolCalls = [];
|
|
463
468
|
}
|
|
@@ -632,6 +637,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
632
637
|
return result;
|
|
633
638
|
};
|
|
634
639
|
const sendStartedAt = Date.now();
|
|
640
|
+
const preSendMs = sendStartedAt - _iterT0;
|
|
641
|
+
const toolResumeMs = _lastToolBatchEndedAt
|
|
642
|
+
? sendStartedAt - _lastToolBatchEndedAt
|
|
643
|
+
: null;
|
|
635
644
|
try { opts.onProviderSendStarted?.(); } catch {}
|
|
636
645
|
const _sendResult = await runWithProviderRequestToolsScope(
|
|
637
646
|
requestToolScope,
|
|
@@ -659,6 +668,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
659
668
|
opts.onToolCall = undefined;
|
|
660
669
|
delete opts.cacheBreakIntent;
|
|
661
670
|
contextOverflowRetryUsed = false;
|
|
671
|
+
// A completed send ends the outage this budget was covering; the next
|
|
672
|
+
// iteration is a fresh request and must get the full replay budget
|
|
673
|
+
// again (mirrors contextOverflowRetryUsed above).
|
|
674
|
+
_transportRetriesUsed = 0;
|
|
662
675
|
// Capture opaque state for the next turn only when the provider
|
|
663
676
|
// explicitly returned the field. Absence means "no update"; an own
|
|
664
677
|
// property with null/undefined means "clear".
|
|
@@ -679,6 +692,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
679
692
|
sessionId,
|
|
680
693
|
iteration: iterations,
|
|
681
694
|
sendMs: Date.now() - sendStartedAt,
|
|
695
|
+
preSendMs,
|
|
696
|
+
toolResumeMs,
|
|
682
697
|
messageCount: Array.isArray(messages) ? messages.length : 0,
|
|
683
698
|
bodyBytesEst: _traceVerbose
|
|
684
699
|
? estimateProviderPayloadBytes(messages, model, sendTools)
|
|
@@ -1050,15 +1065,24 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1050
1065
|
}));
|
|
1051
1066
|
// Settle the stop hook on the batch that actually executed.
|
|
1052
1067
|
_toolFailureStopHook.endBatch(_callsToExecute);
|
|
1068
|
+
const _toolsEndedAt = Date.now();
|
|
1069
|
+
try {
|
|
1070
|
+
opts.onToolPhaseCompleted?.({
|
|
1071
|
+
iteration: nextIteration,
|
|
1072
|
+
calls: _callsToExecute.length,
|
|
1073
|
+
elapsedMs: _toolsEndedAt - _toolsT0,
|
|
1074
|
+
});
|
|
1075
|
+
} catch {}
|
|
1053
1076
|
// Loop-phase timing (diagnostics): where non-model time goes per
|
|
1054
1077
|
// iteration — presend (repair/compact/snapshot), send (provider
|
|
1055
1078
|
// round-trip incl. streaming), tools (batch execution). Gated by the
|
|
1056
1079
|
// same env as [turn-timing] so bench runs opt in via -AgentEnv.
|
|
1057
1080
|
if (process.env.MIXDOG_TURN_TIMING === '1') {
|
|
1058
1081
|
try {
|
|
1059
|
-
process.stderr.write(`[loop-timing] iter=${nextIteration} presend=${
|
|
1082
|
+
process.stderr.write(`[loop-timing] iter=${nextIteration} presend=${preSendMs}ms send=${_sendEndedAt - sendStartedAt}ms tools=${_toolsEndedAt - _toolsT0}ms calls=${_callsToExecute.length}\n`);
|
|
1060
1083
|
} catch { /* diagnostics only */ }
|
|
1061
1084
|
}
|
|
1085
|
+
_lastToolBatchEndedAt = _toolsEndedAt;
|
|
1062
1086
|
_toolBatchJustCompleted = true;
|
|
1063
1087
|
_continuationsSinceToolBatch = 0;
|
|
1064
1088
|
_lastToolBatchHadSleep = _callsToExecute.some(isSleepLikeToolCall);
|
|
@@ -1,13 +1,11 @@
|
|
|
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.
|
|
4
|
-
// streaming
|
|
5
|
-
//
|
|
6
|
-
// wait for their segment bounded by apply_patch barriers. Patches execute
|
|
7
|
-
// serially as barriers, so a failed patch can skip every later side effect.
|
|
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.
|
|
8
6
|
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
9
7
|
import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
|
|
10
|
-
import { _intraTurnSig,
|
|
8
|
+
import { _intraTurnSig, _isReadTool, _isScopedCacheableTool, _stripMcpPrefix } from './loop/tool-classify.mjs';
|
|
11
9
|
import { tryReadCached, tryScopedToolCached } from './read-dedup.mjs';
|
|
12
10
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
13
11
|
import { executeTool } from './loop/tool-exec.mjs';
|
|
@@ -40,11 +38,6 @@ export function createEagerDispatcher({
|
|
|
40
38
|
// resets at the turn boundary without leaking across getIterations().
|
|
41
39
|
const _eagerInFlightSigs = new Map();
|
|
42
40
|
const epoch = { mutation: 0 };
|
|
43
|
-
// True once an apply_patch tool_use has been seen in THIS turn's
|
|
44
|
-
// stream. Before that, every side-effect call streamed so far sits
|
|
45
|
-
// before the first patch in call order (segment 0), so it may start
|
|
46
|
-
// immediately — a patch that arrives later cannot gate an EARLIER call.
|
|
47
|
-
let _streamSeenMutation = false;
|
|
48
41
|
const startEagerTool = (call) => {
|
|
49
42
|
if (!call?.id || pending.has(call.id) || !isParallelDispatchable(call.name)) return null;
|
|
50
43
|
// Never eager-execute a call whose arguments failed to parse
|
|
@@ -162,21 +155,10 @@ export function createEagerDispatcher({
|
|
|
162
155
|
return entry;
|
|
163
156
|
};
|
|
164
157
|
const startEagerRun = (calls, startIndex, dupSet) => {
|
|
165
|
-
const _nextOrderedMutationIndex = calls.findIndex(
|
|
166
|
-
(call, index) => index >= startIndex && _isMutationTool(call?.name),
|
|
167
|
-
);
|
|
168
158
|
for (let j = startIndex; j < calls.length; j += 1) {
|
|
169
159
|
const call = calls[j];
|
|
170
|
-
// Side effects may eager-start in the current segment but not
|
|
171
|
-
// across the next apply_patch barrier. Known read-only work
|
|
172
|
-
// may cross it and is protected by mutation-epoch re-execution.
|
|
173
160
|
if (!call?.id || !isParallelDispatchable(call.name)) continue;
|
|
174
161
|
if (dupSet && dupSet.has(call.id)) continue;
|
|
175
|
-
if (
|
|
176
|
-
_nextOrderedMutationIndex >= 0
|
|
177
|
-
&& j >= _nextOrderedMutationIndex
|
|
178
|
-
&& _isOrderedGateSkippable(call.name)
|
|
179
|
-
) continue;
|
|
180
162
|
// A null return here is NOT a state barrier. It means a
|
|
181
163
|
// non-barrier stub — intra-turn in-flight dup, repeat-failure /
|
|
182
164
|
// cross-turn dedup, pre-dispatch-deny, invalid-args, or a cache
|
|
@@ -187,17 +169,6 @@ export function createEagerDispatcher({
|
|
|
187
169
|
}
|
|
188
170
|
};
|
|
189
171
|
const onToolCall = (call) => {
|
|
190
|
-
if (_isMutationTool(call?.name)) {
|
|
191
|
-
_streamSeenMutation = true;
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
// Declared read-only calls always overlap streaming (epoch guard
|
|
195
|
-
// re-executes them if a later patch lands). Side-effect calls may
|
|
196
|
-
// stream-start only while NO apply_patch has streamed yet: their
|
|
197
|
-
// call-order position is already fixed before the first barrier,
|
|
198
|
-
// matching exactly what startEagerRun would do post-batch. Once a
|
|
199
|
-
// patch has streamed, later side effects wait for their segment.
|
|
200
|
-
if (!isEagerDispatchable(call?.name, tools) && _streamSeenMutation) return;
|
|
201
172
|
startEagerTool(call);
|
|
202
173
|
};
|
|
203
174
|
return { pending, epoch, startEagerTool, startEagerRun, onToolCall };
|
|
@@ -71,6 +71,19 @@ export function isLoopbackHttpUrl(value) {
|
|
|
71
71
|
*/
|
|
72
72
|
export function routeWebFetchCall(call) {
|
|
73
73
|
if (call?.name !== 'web_fetch') return call;
|
|
74
|
+
// A JSON-stringified array ('["https://a","https://b"]') is a common
|
|
75
|
+
// provider serialisation artifact. Parse it back into real URLs so the
|
|
76
|
+
// fetch surface routes and validates them instead of rejecting the call.
|
|
77
|
+
const rawUrl = call?.arguments?.url;
|
|
78
|
+
if (typeof rawUrl === 'string' && /^\s*\[/.test(rawUrl)) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(rawUrl);
|
|
81
|
+
if (Array.isArray(parsed) && parsed.length > 0
|
|
82
|
+
&& parsed.every((item) => typeof item === 'string' && item.trim())) {
|
|
83
|
+
call.arguments.url = parsed.map((item) => item.trim());
|
|
84
|
+
}
|
|
85
|
+
} catch { /* leave the original value for the downstream validator */ }
|
|
86
|
+
}
|
|
74
87
|
const urls = callUrls(call);
|
|
75
88
|
if (!urls.length) return call;
|
|
76
89
|
if (urls.every(isLoopbackHttpUrl)) call.name = 'local_fetch';
|
|
@@ -28,8 +28,8 @@ export function isOutputLimitStopReason(reason) {
|
|
|
28
28
|
// NOT a completed assistant turn even when it holds final-looking text: the
|
|
29
29
|
// provider explicitly stated it intends to keep sampling inside the SAME user
|
|
30
30
|
// turn. Two structured shapes are honored:
|
|
31
|
-
// - `endTurn === false` (
|
|
32
|
-
//
|
|
31
|
+
// - `endTurn === false` (Responses `end_turn=false` on the completed frame,
|
|
32
|
+
// which means the turn needs a follow-up)
|
|
33
33
|
// - Anthropic `stop_reason: 'pause_turn'` (server-side pause; the turn is
|
|
34
34
|
// resumed by replaying the transcript unchanged)
|
|
35
35
|
// Structural only — no lexical/progress-text heuristic is consulted, and a
|
|
@@ -15,30 +15,6 @@ export function _isMutationTool(name) {
|
|
|
15
15
|
const n = _stripMcpPrefix(name);
|
|
16
16
|
return n === 'apply_patch';
|
|
17
17
|
}
|
|
18
|
-
// Side-effect-free read-only tools that may eager-start across an upcoming
|
|
19
|
-
// apply_patch barrier and may keep running after an earlier patch fails.
|
|
20
|
-
// Everything NOT in this set waits for its side-effect segment and is skipped
|
|
21
|
-
// after a failed patch. Kept separate from _isMutationTool, which stays
|
|
22
|
-
// apply_patch-only for epoch counting.
|
|
23
|
-
const ORDERED_GATE_SAFE_READONLY_TOOLS = new Set([
|
|
24
|
-
'read',
|
|
25
|
-
'find',
|
|
26
|
-
'glob',
|
|
27
|
-
'list',
|
|
28
|
-
'grep',
|
|
29
|
-
'code_graph',
|
|
30
|
-
'explore',
|
|
31
|
-
'search',
|
|
32
|
-
'web_fetch',
|
|
33
|
-
]);
|
|
34
|
-
// True when a LATER call in a batch must be skipped because an earlier ordered
|
|
35
|
-
// mutation (apply_patch) already failed. Conservative: only the known
|
|
36
|
-
// side-effect-free read-only tools above keep running; shell, write tools, and
|
|
37
|
-
// unknown MCP tools are skipped so they cannot act on unchanged/partially
|
|
38
|
-
// changed state.
|
|
39
|
-
export function _isOrderedGateSkippable(name) {
|
|
40
|
-
return !ORDERED_GATE_SAFE_READONLY_TOOLS.has(_stripMcpPrefix(name));
|
|
41
|
-
}
|
|
42
18
|
const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
43
19
|
'code_graph',
|
|
44
20
|
'grep',
|
|
@@ -227,36 +227,10 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
|
|
|
227
227
|
}
|
|
228
228
|
if (name === 'apply_patch') {
|
|
229
229
|
const patchArgs = typeof args === 'string' ? { patch: args } : { ...(args || {}) };
|
|
230
|
-
|
|
231
|
-
// one-shot shell path ONLY after the patch applies cleanly; a failed
|
|
232
|
-
// patch skips it. Patch text and shell output return as ONE tool
|
|
233
|
-
// result, so patch+verify costs a single call. Runtime-only knob:
|
|
234
|
-
// stripped before executePatchTool sees the args.
|
|
235
|
-
const postShell = typeof patchArgs.post_shell === 'string' && patchArgs.post_shell.trim()
|
|
236
|
-
? patchArgs.post_shell.trim()
|
|
237
|
-
: null;
|
|
238
|
-
delete patchArgs.post_shell;
|
|
239
|
-
const patchResult = await executePatchTool(name, patchArgs, cwd, {
|
|
230
|
+
return executePatchTool(name, patchArgs, cwd, {
|
|
240
231
|
sessionId: callerSessionId,
|
|
241
232
|
toolCallId: executeOpts.toolCallId || null,
|
|
242
233
|
});
|
|
243
|
-
if (!postShell) return patchResult;
|
|
244
|
-
const patchNorm = normalizeToolEnvelope(patchResult);
|
|
245
|
-
const patchText = typeof patchNorm.result === 'string' ? patchNorm.result : String(patchNorm.result ?? '');
|
|
246
|
-
// Text-based failure detection only: legacy string returns normalize
|
|
247
|
-
// to explicitSuccess:false even on success, so that flag is unusable here.
|
|
248
|
-
const patchFailed = /^Error[\s:[]/.test(patchText.trimStart());
|
|
249
|
-
if (patchFailed) {
|
|
250
|
-
return `${patchText}\n--- post_shell skipped: patch failed ---`;
|
|
251
|
-
}
|
|
252
|
-
const shellRes = await executeBuiltinTool('shell', { command: postShell }, cwd, completionToolOpts);
|
|
253
|
-
const shellNorm = normalizeToolEnvelope(shellRes);
|
|
254
|
-
const shellText = typeof shellNorm.result === 'string' ? shellNorm.result : String(shellNorm.result ?? '');
|
|
255
|
-
const shellFailed = /^Error[\s:[]/.test(shellText.trimStart());
|
|
256
|
-
const header = shellFailed
|
|
257
|
-
? '--- post_shell FAILED (patch is applied; fix and re-verify) ---'
|
|
258
|
-
: '--- post_shell ---';
|
|
259
|
-
return `${patchText}\n\n${header}\n${shellText}`;
|
|
260
234
|
}
|
|
261
235
|
if (isBuiltinTool(name)) {
|
|
262
236
|
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
isSkillDisabled,
|
|
13
13
|
} from '../../context/collect.mjs';
|
|
14
14
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
15
|
-
import { _isMutationTool } from './tool-classify.mjs';
|
|
16
15
|
|
|
17
16
|
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
18
17
|
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
@@ -41,16 +40,13 @@ export function isEagerDispatchable(name, tools) {
|
|
|
41
40
|
return set.has(name);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
// Full-parallel dispatch policy: EVERY tool call in an assistant turn
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// invalidation sequencing. isEagerDispatchable above remains the READ-ONLY
|
|
50
|
-
// classifier used for dedup eligibility, steering, and post-mutation result
|
|
51
|
-
// invalidation.
|
|
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.
|
|
46
|
+
// isEagerDispatchable above remains the READ-ONLY classifier used for dedup
|
|
47
|
+
// eligibility, steering, and post-mutation result invalidation.
|
|
52
48
|
export function isParallelDispatchable(name) {
|
|
53
|
-
return typeof name === 'string' && name.length > 0
|
|
49
|
+
return typeof name === 'string' && name.length > 0;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
// Read-only is necessary but not sufficient for result deduplication. Loader
|
|
@@ -645,10 +645,15 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
645
645
|
onProviderSendStarted: () => {
|
|
646
646
|
_turnInterruption.markProviderSendStarted();
|
|
647
647
|
_scheduleTurnCheckpoint(true);
|
|
648
|
+
try { askOpts?.onProviderSendStarted?.(); } catch {}
|
|
648
649
|
},
|
|
649
650
|
onToolPhaseStarted: () => {
|
|
650
651
|
_turnInterruption.markToolPhaseStarted();
|
|
651
652
|
_scheduleTurnCheckpoint(true);
|
|
653
|
+
try { askOpts?.onToolPhaseStarted?.(); } catch {}
|
|
654
|
+
},
|
|
655
|
+
onToolPhaseCompleted: (detail) => {
|
|
656
|
+
try { askOpts?.onToolPhaseCompleted?.(detail); } catch {}
|
|
652
657
|
},
|
|
653
658
|
onUsageDelta: (d) => {
|
|
654
659
|
persistIterationMetrics(d).catch(() => {});
|