mixdog 0.9.95 → 0.9.97
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/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- 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 +7 -4
- 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 +21 -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/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- 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/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- 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 +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -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 +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- 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/session-lock.mjs +38 -2
- 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-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 +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -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 +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- 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/fs-reachability.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 +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -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 +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -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/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- 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 +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- 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 +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- 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 +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- 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 +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -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 +13 -34
- 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 +9 -6
- 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-config-flags.mjs +1 -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-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -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/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- 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 +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- 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 +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- 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 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- 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/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- 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/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- 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} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- 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 +31 -33
- 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 +10 -3
- 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/ToolExecution.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/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- 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 +38 -4
- 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 +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- 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 +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- 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-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- 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
|
@@ -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
|
+
});
|
|
@@ -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.
|
|
@@ -403,23 +403,26 @@ 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();
|
|
416
421
|
return name === 'sleep' || name.endsWith('/sleep') || name.endsWith('.sleep');
|
|
417
422
|
};
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
// against the host/plugin root instead of the worker workspace.
|
|
422
|
-
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);
|
|
423
426
|
// Staged pre-cap warnings + one true hard stop. The ONLY count-based
|
|
424
427
|
// forced termination is the hard cap at maxLoopIterations (default 200):
|
|
425
428
|
// a genuine runaway guard. Before it, staged warnings fire at 50%/75%/90%
|
|
@@ -440,6 +443,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
440
443
|
Math.floor(maxLoopIterations * 0.9),
|
|
441
444
|
];
|
|
442
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);
|
|
443
450
|
const _iterT0 = Date.now();
|
|
444
451
|
throwIfAborted();
|
|
445
452
|
if (iterations >= maxLoopIterations) {
|
|
@@ -457,7 +464,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
457
464
|
// non-empty final so callers never see an empty response.
|
|
458
465
|
if (response && !String(response.content || '').trim()) {
|
|
459
466
|
response.content = sessionAgent === 'explorer'
|
|
460
|
-
? 'EXPLORATION_FAILED'
|
|
467
|
+
? 'EXPLORATION_FAILED [iteration-cap] — explorer hit its tool-turn cap with zero anchors; retry once with changed tokens or a narrower root'
|
|
461
468
|
: '[iteration cap reached before final text]';
|
|
462
469
|
if (Array.isArray(response.toolCalls)) response.toolCalls = [];
|
|
463
470
|
}
|
|
@@ -632,6 +639,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
632
639
|
return result;
|
|
633
640
|
};
|
|
634
641
|
const sendStartedAt = Date.now();
|
|
642
|
+
const preSendMs = sendStartedAt - _iterT0;
|
|
643
|
+
const toolResumeMs = _lastToolBatchEndedAt
|
|
644
|
+
? sendStartedAt - _lastToolBatchEndedAt
|
|
645
|
+
: null;
|
|
635
646
|
try { opts.onProviderSendStarted?.(); } catch {}
|
|
636
647
|
const _sendResult = await runWithProviderRequestToolsScope(
|
|
637
648
|
requestToolScope,
|
|
@@ -659,6 +670,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
659
670
|
opts.onToolCall = undefined;
|
|
660
671
|
delete opts.cacheBreakIntent;
|
|
661
672
|
contextOverflowRetryUsed = false;
|
|
673
|
+
// A completed send ends the outage this budget was covering; the next
|
|
674
|
+
// iteration is a fresh request and must get the full replay budget
|
|
675
|
+
// again (mirrors contextOverflowRetryUsed above).
|
|
676
|
+
_transportRetriesUsed = 0;
|
|
662
677
|
// Capture opaque state for the next turn only when the provider
|
|
663
678
|
// explicitly returned the field. Absence means "no update"; an own
|
|
664
679
|
// property with null/undefined means "clear".
|
|
@@ -679,6 +694,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
679
694
|
sessionId,
|
|
680
695
|
iteration: iterations,
|
|
681
696
|
sendMs: Date.now() - sendStartedAt,
|
|
697
|
+
preSendMs,
|
|
698
|
+
toolResumeMs,
|
|
682
699
|
messageCount: Array.isArray(messages) ? messages.length : 0,
|
|
683
700
|
bodyBytesEst: _traceVerbose
|
|
684
701
|
? estimateProviderPayloadBytes(messages, model, sendTools)
|
|
@@ -1050,15 +1067,24 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1050
1067
|
}));
|
|
1051
1068
|
// Settle the stop hook on the batch that actually executed.
|
|
1052
1069
|
_toolFailureStopHook.endBatch(_callsToExecute);
|
|
1070
|
+
const _toolsEndedAt = Date.now();
|
|
1071
|
+
try {
|
|
1072
|
+
opts.onToolPhaseCompleted?.({
|
|
1073
|
+
iteration: nextIteration,
|
|
1074
|
+
calls: _callsToExecute.length,
|
|
1075
|
+
elapsedMs: _toolsEndedAt - _toolsT0,
|
|
1076
|
+
});
|
|
1077
|
+
} catch {}
|
|
1053
1078
|
// Loop-phase timing (diagnostics): where non-model time goes per
|
|
1054
1079
|
// iteration — presend (repair/compact/snapshot), send (provider
|
|
1055
1080
|
// round-trip incl. streaming), tools (batch execution). Gated by the
|
|
1056
1081
|
// same env as [turn-timing] so bench runs opt in via -AgentEnv.
|
|
1057
1082
|
if (process.env.MIXDOG_TURN_TIMING === '1') {
|
|
1058
1083
|
try {
|
|
1059
|
-
process.stderr.write(`[loop-timing] iter=${nextIteration} presend=${
|
|
1084
|
+
process.stderr.write(`[loop-timing] iter=${nextIteration} presend=${preSendMs}ms send=${_sendEndedAt - sendStartedAt}ms tools=${_toolsEndedAt - _toolsT0}ms calls=${_callsToExecute.length}\n`);
|
|
1060
1085
|
} catch { /* diagnostics only */ }
|
|
1061
1086
|
}
|
|
1087
|
+
_lastToolBatchEndedAt = _toolsEndedAt;
|
|
1062
1088
|
_toolBatchJustCompleted = true;
|
|
1063
1089
|
_continuationsSinceToolBatch = 0;
|
|
1064
1090
|
_lastToolBatchHadSleep = _callsToExecute.some(isSleepLikeToolCall);
|
|
@@ -1,13 +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.
|
|
4
|
-
// streaming.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// serially as barriers, so a failed patch can skip every later side effect.
|
|
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.
|
|
8
7
|
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
9
8
|
import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
_intraTurnSig,
|
|
11
|
+
_isMutationTool,
|
|
12
|
+
_isReadTool,
|
|
13
|
+
_isScopedCacheableTool,
|
|
14
|
+
_isShellTool,
|
|
15
|
+
_stripMcpPrefix,
|
|
16
|
+
} from './loop/tool-classify.mjs';
|
|
11
17
|
import { tryReadCached, tryScopedToolCached } from './read-dedup.mjs';
|
|
12
18
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
13
19
|
import { executeTool } from './loop/tool-exec.mjs';
|
|
@@ -20,6 +26,10 @@ export function createEagerDispatcher({
|
|
|
20
26
|
executeToolFn = executeTool,
|
|
21
27
|
}) {
|
|
22
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();
|
|
23
33
|
// Streaming-time intra-turn dedup. When the LLM emits two
|
|
24
34
|
// tool_use blocks with identical (name, args) signatures in
|
|
25
35
|
// sequence, the provider's onToolCall fires for both BEFORE
|
|
@@ -40,11 +50,6 @@ export function createEagerDispatcher({
|
|
|
40
50
|
// resets at the turn boundary without leaking across getIterations().
|
|
41
51
|
const _eagerInFlightSigs = new Map();
|
|
42
52
|
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
53
|
const startEagerTool = (call) => {
|
|
49
54
|
if (!call?.id || pending.has(call.id) || !isParallelDispatchable(call.name)) return null;
|
|
50
55
|
// Never eager-execute a call whose arguments failed to parse
|
|
@@ -97,9 +102,11 @@ export function createEagerDispatcher({
|
|
|
97
102
|
// this call there, never start it eagerly here.
|
|
98
103
|
if (preDispatchDenyForSession(sessionRef, call, toolKind) !== null) return null;
|
|
99
104
|
const entry = { startedAt: Date.now(), endedAt: null, mutationEpoch: epoch.mutation };
|
|
105
|
+
const precedingPatches = _isShellTool(call.name) ? patchBarrier : null;
|
|
100
106
|
if (_dedupEligible) _eagerInFlightSigs.set(_sig, call.id);
|
|
101
107
|
entry.promise = (async () => {
|
|
102
108
|
try {
|
|
109
|
+
if (precedingPatches) await precedingPatches;
|
|
103
110
|
await opts.beforeToolExecution?.();
|
|
104
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 }) };
|
|
105
112
|
} catch (error) {
|
|
@@ -159,24 +166,17 @@ export function createEagerDispatcher({
|
|
|
159
166
|
return settled;
|
|
160
167
|
});
|
|
161
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
|
+
}
|
|
162
173
|
return entry;
|
|
163
174
|
};
|
|
164
175
|
const startEagerRun = (calls, startIndex, dupSet) => {
|
|
165
|
-
const _nextOrderedMutationIndex = calls.findIndex(
|
|
166
|
-
(call, index) => index >= startIndex && _isMutationTool(call?.name),
|
|
167
|
-
);
|
|
168
176
|
for (let j = startIndex; j < calls.length; j += 1) {
|
|
169
177
|
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
178
|
if (!call?.id || !isParallelDispatchable(call.name)) continue;
|
|
174
179
|
if (dupSet && dupSet.has(call.id)) continue;
|
|
175
|
-
if (
|
|
176
|
-
_nextOrderedMutationIndex >= 0
|
|
177
|
-
&& j >= _nextOrderedMutationIndex
|
|
178
|
-
&& _isOrderedGateSkippable(call.name)
|
|
179
|
-
) continue;
|
|
180
180
|
// A null return here is NOT a state barrier. It means a
|
|
181
181
|
// non-barrier stub — intra-turn in-flight dup, repeat-failure /
|
|
182
182
|
// cross-turn dedup, pre-dispatch-deny, invalid-args, or a cache
|
|
@@ -187,17 +187,6 @@ export function createEagerDispatcher({
|
|
|
187
187
|
}
|
|
188
188
|
};
|
|
189
189
|
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
190
|
startEagerTool(call);
|
|
202
191
|
};
|
|
203
192
|
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',
|