mixdog 0.9.94 → 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 +109 -0
- package/package.json +26 -13
- 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/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +20 -35
- 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/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +13 -2
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +29 -14
- 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-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- 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/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- 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 +29 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +13 -14
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +82 -22
- 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 +40 -36
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -33
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- 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 +12 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +168 -83
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +34 -18
- 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 +89 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +6 -39
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -6
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -4
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +124 -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/lib/grep-context-expander.mjs +7 -16
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- 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-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.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 +24 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +20 -43
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +76 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +38 -13
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +138 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +177 -16
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +21 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +217 -25
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +60 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/config.mjs +2 -2
- package/src/runtime/channels/lib/inbound-handler.mjs +1 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +3 -3
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -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 +7 -11
- 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 +133 -4
- package/src/runtime/shared/background-tasks.mjs +6 -3
- 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 -58
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +2 -7
- package/src/runtime/shared/turn-snapshot.mjs +411 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +552 -0
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/runtime-core.mjs +57 -19
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +11 -2
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +139 -23
- 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 +25 -28
- 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/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +631 -0
- package/src/standalone/channel-admin.mjs +12 -12
- package/src/standalone/channel-daemon-client.mjs +17 -1
- package/src/standalone/channel-daemon-transport.mjs +231 -9
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +1069 -0
- package/src/standalone/engine-daemon-protocol.mjs +32 -0
- package/src/standalone/engine-daemon-service.mjs +1108 -0
- package/src/standalone/engine-daemon-transport.mjs +794 -0
- 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 +92 -64
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +10 -2
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +20 -20
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +24 -20
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +59 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +16 -9
- package/src/tui/app/settings-picker.mjs +60 -65
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +57 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- 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 +39 -17
- package/src/tui/app/use-transcript-scroll.mjs +39 -10
- package/src/tui/app/use-transcript-window.mjs +62 -14
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +81 -38
- package/src/tui/components/Spinner.jsx +89 -95
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- 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 +1694 -10633
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +110 -3
- package/src/tui/engine/session-api-ext.mjs +24 -5
- package/src/tui/engine/session-api.mjs +165 -55
- package/src/tui/engine/session-flow.mjs +50 -5
- package/src/tui/engine/tool-card-results.mjs +10 -2
- package/src/tui/engine/tool-result-text.mjs +10 -0
- package/src/tui/engine/turn.mjs +11 -64
- package/src/tui/engine-local-session.mjs +1124 -0
- package/src/tui/engine.mjs +16 -1065
- package/src/tui/index.jsx +41 -4
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +83 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -104,6 +104,15 @@ export function classifyError(err) {
|
|
|
104
104
|
if (err.truncatedStream === true || err.code === 'TRUNCATED_STREAM') return 'transient'
|
|
105
105
|
|
|
106
106
|
if (TRANSIENT_STATUSES.has(status) || (status >= 500 && status < 600)) return 'transient'
|
|
107
|
+
// A stream that closed WITHOUT its terminal frame is a transport symptom,
|
|
108
|
+
// not a model verdict: the socket carries no HTTP status and no Node errno,
|
|
109
|
+
// so without this it classified as 'unknown' and no loop-level replay was
|
|
110
|
+
// ever attempted (observed live: pooled WS retired by the server between
|
|
111
|
+
// turns → close 1000 before response.created → the whole turn failed).
|
|
112
|
+
// Reference behavior retries the same disconnect (codex `CodexErr::Stream`
|
|
113
|
+
// is_retryable, cc falls back/retries the stream); the exposure deny above
|
|
114
|
+
// still fails closed for anything already relayed or dispatched.
|
|
115
|
+
if (isNonTerminalStreamClose(err)) return 'transient'
|
|
107
116
|
|
|
108
117
|
// Socket-level codes (Node errno) — DNS / reset / refused / timeout are all
|
|
109
118
|
// transient: we can retry the same request and may succeed.
|
|
@@ -124,6 +133,50 @@ const TRANSIENT_ERROR_CODES = new Set([
|
|
|
124
133
|
'EPROVIDERTIMEOUT', 'EGEMINITIMEOUT', 'ESTREAMSTALL', 'EWSACQUIRETIMEOUT',
|
|
125
134
|
])
|
|
126
135
|
|
|
136
|
+
// WebSocket close codes that can end a Responses stream BEFORE its terminal
|
|
137
|
+
// frame. 1000/1001/1005 are "nominal" closes the server sends when it retires
|
|
138
|
+
// a pooled socket; 1006/1011/1012 are abnormal/overload closes. None of them
|
|
139
|
+
// is a completed turn, and all are safe to re-issue when nothing was exposed.
|
|
140
|
+
// 4000 is our own local stall close and keeps the stall classification path.
|
|
141
|
+
const NON_TERMINAL_STREAM_CLOSE_CODES = new Set([1000, 1001, 1005, 1006, 1011, 1012])
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* True when the error describes a stream socket that closed without the
|
|
145
|
+
* provider's terminal frame (response.completed / message_stop). Replay
|
|
146
|
+
* PERMISSION is still owned by the stream-outcome contract; this predicate
|
|
147
|
+
* only answers "was this a transport-level disconnect".
|
|
148
|
+
*/
|
|
149
|
+
export function isNonTerminalStreamClose(err) {
|
|
150
|
+
if (!err || typeof err !== 'object') return false
|
|
151
|
+
const code = Number(err.wsCloseCode ?? err.streamCloseCode ?? 0) || 0
|
|
152
|
+
if (!NON_TERMINAL_STREAM_CLOSE_CODES.has(code)) return false
|
|
153
|
+
return readStreamOutcome(err).terminalObserved !== true
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Should this failed stream be re-issued as a NON-STREAMING request?
|
|
158
|
+
*
|
|
159
|
+
* cc's last safety net: when a stream dies it repeats the same request with
|
|
160
|
+
* `stream:false` instead of failing the turn (claude.ts, gated off only when
|
|
161
|
+
* streaming tool execution could double-run a tool). MixDog dispatches tools
|
|
162
|
+
* eagerly, so this stays deliberately narrower than cc: only a stream that
|
|
163
|
+
* exposed NOTHING qualifies. An exposed stream is already covered by the
|
|
164
|
+
* loop-level retraction replay (send-with-recovery), which asks the owner to
|
|
165
|
+
* withdraw the rendered characters first.
|
|
166
|
+
*/
|
|
167
|
+
export function canFallbackNonStreaming(err, { signal } = {}) {
|
|
168
|
+
if (!err || signal?.aborted) return false
|
|
169
|
+
const outcome = readStreamOutcome(err)
|
|
170
|
+
// A completed turn needs no fallback; a user cancel must never be re-issued.
|
|
171
|
+
if (outcome.terminalObserved || outcome.userAbort) return false
|
|
172
|
+
// Exposure/dispatch fails closed — re-running would duplicate output or a
|
|
173
|
+
// side effect.
|
|
174
|
+
if (outcome.replaySafe !== true) return false
|
|
175
|
+
return classifyError(err) === 'transient'
|
|
176
|
+
|| outcome.stallObserved === true
|
|
177
|
+
|| outcome.truncatedStream === true
|
|
178
|
+
}
|
|
179
|
+
|
|
127
180
|
function boundedCauseChain(err) {
|
|
128
181
|
const chain = []
|
|
129
182
|
const seen = new Set()
|
|
@@ -252,7 +305,7 @@ function isRetryable(err) {
|
|
|
252
305
|
return classifyError(err) === 'transient'
|
|
253
306
|
}
|
|
254
307
|
|
|
255
|
-
/**
|
|
308
|
+
/** Anthropic request budget: 10 retries (11 attempts).
|
|
256
309
|
* CLAUDE_CODE_MAX_RETRIES is intentionally read per request for reload/tests.
|
|
257
310
|
* The upper bound prevents an accidental unbounded retry loop. */
|
|
258
311
|
export function anthropicMaxAttempts() {
|
|
@@ -262,16 +315,16 @@ export function anthropicMaxAttempts() {
|
|
|
262
315
|
return retries + 1
|
|
263
316
|
}
|
|
264
317
|
|
|
265
|
-
//
|
|
318
|
+
// Anthropic retry defaults: 500ms exponential backoff,
|
|
266
319
|
// capped at 32s, with positive-only jitter up to 25% of the base delay.
|
|
267
|
-
// The leading duplicate accounts for
|
|
320
|
+
// The leading duplicate accounts for the sleep-before-attempt index:
|
|
268
321
|
// retry attempt 2 reads index 1.
|
|
269
322
|
export const ANTHROPIC_RETRY_BACKOFF_MS = Object.freeze([
|
|
270
323
|
500, 500, 1000, 2000, 4000, 8000, 16000, 32000, 32000, 32000, 32000,
|
|
271
324
|
])
|
|
272
325
|
export const ANTHROPIC_RETRY_JITTER_RATIO = 0.25
|
|
273
326
|
|
|
274
|
-
//
|
|
327
|
+
// The Anthropic SDK client defaults API_TIMEOUT_MS to ten minutes.
|
|
275
328
|
// Read per request, like CLAUDE_CODE_MAX_RETRIES, so env reload/tests work.
|
|
276
329
|
export function anthropicRequestTimeoutMs() {
|
|
277
330
|
const parsed = Number.parseInt(process.env.API_TIMEOUT_MS || '', 10)
|
|
@@ -318,10 +371,10 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
|
|
|
318
371
|
// Mid-stream 'stream_stalled' recoveries retry in place, which is right for a
|
|
319
372
|
// one-off blip but lets a chronically dying stream burn a whole task budget
|
|
320
373
|
// slowly (observed live: one send stretched 149s→298s→556s across stall
|
|
321
|
-
// retries before the agent deadline killed the task).
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
374
|
+
// retries before the agent deadline killed the task). A stalling stream is
|
|
375
|
+
// bounded instead of retried forever: a request is capped at ~300s wall
|
|
376
|
+
// clock (API_TIMEOUT_MS) and a stream dies after one 300s silent gap
|
|
377
|
+
// (stream idle timeout). This guard is the equivalent for our in-place
|
|
325
378
|
// recovery: the clock starts at the FIRST stall of a send, and stall-classified
|
|
326
379
|
// retries are allowed only inside that window; past it the stall error
|
|
327
380
|
// surfaces so loop-level transport retry issues a FRESH request. Healthy
|
|
@@ -357,7 +410,7 @@ export function createStallRetryBudget(budgetMs = STREAM_STALL_RETRY_BUDGET_MS,
|
|
|
357
410
|
// branched on a hardcoded provider name.
|
|
358
411
|
|
|
359
412
|
// F) Retry-budget profiles as DATA. The numbers live ONLY here now.
|
|
360
|
-
// ws.*Retries (5) — one
|
|
413
|
+
// ws.*Retries (5) — one Responses stream retry budget.
|
|
361
414
|
// sse.defaultRetries (3) — anthropic single-shot SSE mid-stream budget.
|
|
362
415
|
export const MIDSTREAM_RETRY_POLICY = {
|
|
363
416
|
ws: { transientCloseRetries: 5, defaultRetries: 5, backoff: [250, 1000, 2000, 4000, 5000] },
|
|
@@ -435,11 +488,13 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
435
488
|
}
|
|
436
489
|
if (!state.sawResponseCreated) {
|
|
437
490
|
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
491
|
+
// A close before response.created has not produced any response bytes to
|
|
492
|
+
// the caller, so it is safe to reconnect and replay under the bounded
|
|
493
|
+
// retry policy (text/tool emission was denied above before this gate).
|
|
494
|
+
// NOMINAL closes count too: a pooled socket the server retires between
|
|
495
|
+
// turns closes with 1000/1001/1005 and must reconnect fresh — treating
|
|
496
|
+
// that as terminal killed the turn instead of reissuing it.
|
|
497
|
+
if (!NON_TERMINAL_STREAM_CLOSE_CODES.has(closeCode)) return null
|
|
443
498
|
}
|
|
444
499
|
if (state.userAbort) return null
|
|
445
500
|
|
|
@@ -465,7 +520,11 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
465
520
|
if (closeCode === 1012) return _allowMidstream('ws_1012', attemptIndex, policy)
|
|
466
521
|
if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null
|
|
467
522
|
if (closeCode === 4000) return _allowMidstream('ws_4000', attemptIndex, policy)
|
|
468
|
-
|
|
523
|
+
// Nominal close without the terminal frame — before OR after
|
|
524
|
+
// response.created. Only a completed turn is terminal here.
|
|
525
|
+
if ((closeCode === 1000 || closeCode === 1001 || closeCode === 1005) && !state.sawCompleted) {
|
|
526
|
+
return _allowMidstream(`ws_${closeCode}`, attemptIndex, policy)
|
|
527
|
+
}
|
|
469
528
|
|
|
470
529
|
const failed = err?.responseFailed || state.responseFailedPayload
|
|
471
530
|
if (failed) {
|
|
@@ -490,7 +549,7 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
490
549
|
}
|
|
491
550
|
|
|
492
551
|
// Explicit `response.failed` error codes/types that describe a transport-level
|
|
493
|
-
// interruption (
|
|
552
|
+
// interruption (these are retryable); every other code is terminal.
|
|
494
553
|
const RESPONSE_FAILED_CODE_CLASSIFIERS = new Map([
|
|
495
554
|
['stream_disconnected', 'response_failed_disconnected'],
|
|
496
555
|
['network_error', 'response_failed_network'],
|
|
@@ -536,7 +595,8 @@ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
|
536
595
|
const TRANSPORT_FALLBACK_CLASSIFIERS = new Set([
|
|
537
596
|
'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
|
|
538
597
|
'first_byte_timeout',
|
|
539
|
-
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', '
|
|
598
|
+
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_1001', 'ws_1005', 'ws_4000',
|
|
599
|
+
'agent_stall', 'stream_stalled',
|
|
540
600
|
'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
|
|
541
601
|
'ws_send_failed',
|
|
542
602
|
])
|
|
@@ -550,7 +610,7 @@ export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
|
|
|
550
610
|
if (signal?.aborted) return false
|
|
551
611
|
// Transport fallback re-issues the request on another transport: it is a
|
|
552
612
|
// replay, so the exposure deny applies. Eligibility itself stays typed
|
|
553
|
-
// (status / errno / classifier)
|
|
613
|
+
// (status / errno / classifier) for the WS→HTTPS switch.
|
|
554
614
|
if (readStreamOutcome(err).replaySafe !== true) return false
|
|
555
615
|
const status = Number(err?.httpStatus || err?.status || 0)
|
|
556
616
|
// 401 is auth recovery, never transport fallback. 426 is the explicit
|
|
@@ -660,7 +720,7 @@ function _sleepChunkWithAbort(ms, signal, sleepFn, abortMessage) {
|
|
|
660
720
|
// E) Handshake classifier (moved here from openai-oauth-ws). Default-deny:
|
|
661
721
|
// anything not recognized as transient returns null. HTTP 401 is reserved
|
|
662
722
|
// for auth recovery and 426 for immediate HTTPS fallback. The OpenAI OAuth
|
|
663
|
-
// caller opts out of 429 retries (
|
|
723
|
+
// caller opts out of 429 retries (retry429:false); all other callers
|
|
664
724
|
// retain the historical retryable UnexpectedStatus policy.
|
|
665
725
|
export function classifyHandshakeError(err, { retry429 = true } = {}) {
|
|
666
726
|
if (!err) return null
|
|
@@ -764,8 +824,8 @@ export async function withRetry(fn, opts = {}) {
|
|
|
764
824
|
// an eligible failure is actually retried remains the typed question
|
|
765
825
|
// resolved by classifyError()/status below.
|
|
766
826
|
if (readStreamOutcome(caught).replaySafe !== true) throw caught
|
|
767
|
-
//
|
|
768
|
-
//
|
|
827
|
+
// x-should-retry:false is an explicit server veto on retrying and is
|
|
828
|
+
// honored as-is.
|
|
769
829
|
// Keep this ahead of status defaults, including the request-local 429 path.
|
|
770
830
|
const shouldRetryHeader = _headerValue(
|
|
771
831
|
caught?.headers || caught?.response?.headers || caught?.data?.responseHeaders,
|
|
@@ -786,7 +846,7 @@ export async function withRetry(fn, opts = {}) {
|
|
|
786
846
|
}
|
|
787
847
|
continue
|
|
788
848
|
}
|
|
789
|
-
//
|
|
849
|
+
// The optional model fallback fires on the third 529. This
|
|
790
850
|
// remains opt-in: providers pass fallbackModel only when the caller set
|
|
791
851
|
// one. The hard progress veto above must run first so fallback can never
|
|
792
852
|
// replay partial thinking/tool output.
|
|
@@ -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
|
+
});
|