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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
3
|
-
import { withRetry } from './retry-classifier.mjs';
|
|
3
|
+
import { canFallbackNonStreaming, withRetry } from './retry-classifier.mjs';
|
|
4
4
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
5
5
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
6
6
|
import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
|
|
@@ -69,6 +69,16 @@ function loadOpenAI() {
|
|
|
69
69
|
return _OpenAI;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Grok OAuth creates its credential-bound inner provider only when send() has
|
|
73
|
+
// the final token and request headers. Let the process-wide provider warmup pay
|
|
74
|
+
// the OpenAI SDK + undici pool load before that first request reaches the hot
|
|
75
|
+
// path, without constructing a fake credential-bound client.
|
|
76
|
+
export function preloadOpenAICompatRuntime() {
|
|
77
|
+
const OpenAI = loadOpenAI();
|
|
78
|
+
getLlmDispatcher();
|
|
79
|
+
return OpenAI;
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
function attachCompletedWarmup(err, warmup) {
|
|
73
83
|
if (!err || !warmup?.usage) return err;
|
|
74
84
|
try {
|
|
@@ -238,7 +248,8 @@ export class OpenAICompatProvider {
|
|
|
238
248
|
// extraHeaders behave exactly as before.
|
|
239
249
|
this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(config.extraHeaders || {}) };
|
|
240
250
|
this.defaultModel = preset?.defaultModel || 'default';
|
|
241
|
-
|
|
251
|
+
const OpenAI = preloadOpenAICompatRuntime();
|
|
252
|
+
this.client = new OpenAI({
|
|
242
253
|
baseURL,
|
|
243
254
|
apiKey,
|
|
244
255
|
defaultHeaders: this.defaultHeaders,
|
|
@@ -253,6 +264,12 @@ export class OpenAICompatProvider {
|
|
|
253
264
|
// a harmless no-op then.
|
|
254
265
|
fetchOptions: { dispatcher: getLlmDispatcher() },
|
|
255
266
|
});
|
|
267
|
+
// Provider registry initialization normally runs during daemon warmup.
|
|
268
|
+
// Start the origin handshake there; the per-send call below remains as
|
|
269
|
+
// the TTL-gated rewarm after long idle gaps.
|
|
270
|
+
if (this.config?.preconnect !== false) {
|
|
271
|
+
try { this._preconnectFn(this.baseURL); } catch { /* best-effort */ }
|
|
272
|
+
}
|
|
256
273
|
}
|
|
257
274
|
get _preconnectFn() {
|
|
258
275
|
return typeof this.config?.preconnectFn === 'function'
|
|
@@ -420,44 +437,52 @@ export class OpenAICompatProvider {
|
|
|
420
437
|
params.stream_options = { include_usage: true };
|
|
421
438
|
let assembled;
|
|
422
439
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
440
|
+
try {
|
|
441
|
+
assembled = await withRetry(
|
|
442
|
+
async ({ signal: attemptSignal }) => {
|
|
443
|
+
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
444
|
+
const stream = await withRetry(
|
|
445
|
+
({ signal: openSignal }) => this.client.chat.completions.create(params, { signal: openSignal }),
|
|
446
|
+
{
|
|
447
|
+
signal: attemptSignal,
|
|
448
|
+
// Single attempt: this inner wrapper exists only to
|
|
449
|
+
// apply the first-byte per-attempt timeout. Retry is
|
|
450
|
+
// owned by the outer withRetry — nesting retry loops
|
|
451
|
+
// here multiplied tail latency (5x5).
|
|
452
|
+
maxAttempts: 1,
|
|
453
|
+
perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
454
|
+
perAttemptLabel: `${this.name} first byte`,
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
try { opts.onStageChange?.('streaming'); } catch { /* heartbeat best-effort */ }
|
|
458
|
+
return consumeCompatChatCompletionStream(stream, {
|
|
429
459
|
signal: attemptSignal,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
460
|
+
label: this.name,
|
|
461
|
+
onStreamDelta: opts.onStreamDelta,
|
|
462
|
+
onToolCall: opts.onToolCall,
|
|
463
|
+
onTextDelta: opts.onTextDelta,
|
|
464
|
+
parseToolCalls,
|
|
465
|
+
// Known tool names for the leaked-tool-call guard:
|
|
466
|
+
// recovered leaked calls only synthesize when they name
|
|
467
|
+
// a tool actually offered to this request.
|
|
468
|
+
knownToolNames: knownToolNamesFromOpenAITools(params.tools),
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
signal: totalSignal.signal,
|
|
473
|
+
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
474
|
+
const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
|
|
475
|
+
process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
|
|
437
476
|
},
|
|
438
|
-
);
|
|
439
|
-
try { opts.onStageChange?.('streaming'); } catch { /* heartbeat best-effort */ }
|
|
440
|
-
return consumeCompatChatCompletionStream(stream, {
|
|
441
|
-
signal: attemptSignal,
|
|
442
|
-
label: this.name,
|
|
443
|
-
onStreamDelta: opts.onStreamDelta,
|
|
444
|
-
onToolCall: opts.onToolCall,
|
|
445
|
-
onTextDelta: opts.onTextDelta,
|
|
446
|
-
parseToolCalls,
|
|
447
|
-
// Known tool names for the leaked-tool-call guard:
|
|
448
|
-
// recovered leaked calls only synthesize when they name
|
|
449
|
-
// a tool actually offered to this request.
|
|
450
|
-
knownToolNames: knownToolNamesFromOpenAITools(params.tools),
|
|
451
|
-
});
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
signal: totalSignal.signal,
|
|
455
|
-
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
456
|
-
const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
|
|
457
|
-
process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
|
|
458
477
|
},
|
|
459
|
-
|
|
460
|
-
)
|
|
478
|
+
);
|
|
479
|
+
} catch (streamErr) {
|
|
480
|
+
const recovered = await this._recoverCompatNonStreaming({
|
|
481
|
+
streamErr, params, opts, signal: totalSignal.signal, useModel,
|
|
482
|
+
});
|
|
483
|
+
if (!recovered) throw streamErr;
|
|
484
|
+
assembled = recovered;
|
|
485
|
+
}
|
|
461
486
|
} finally {
|
|
462
487
|
totalSignal.cleanup();
|
|
463
488
|
}
|
|
@@ -567,6 +592,83 @@ export class OpenAICompatProvider {
|
|
|
567
592
|
})() : undefined,
|
|
568
593
|
};
|
|
569
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* cc parity (claude.ts:2504): when a stream dies, re-issue the SAME request
|
|
597
|
+
* with `stream:false` rather than failing the turn. Narrower than cc on
|
|
598
|
+
* purpose — canFallbackNonStreaming() clears only a stream that exposed
|
|
599
|
+
* NOTHING, so an eagerly dispatched tool can never run twice and rendered
|
|
600
|
+
* text is never duplicated (that case belongs to the loop's retraction
|
|
601
|
+
* replay). Returns the same assembled shape the stream consumer produces,
|
|
602
|
+
* or null when the failure is ineligible or the fallback itself fails.
|
|
603
|
+
*/
|
|
604
|
+
async _recoverCompatNonStreaming({ streamErr, params, opts, signal, useModel }) {
|
|
605
|
+
if (!canFallbackNonStreaming(streamErr, { signal })) return null;
|
|
606
|
+
const nonStreamParams = { ...params, stream: false };
|
|
607
|
+
delete nonStreamParams.stream_options;
|
|
608
|
+
let response;
|
|
609
|
+
try {
|
|
610
|
+
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
611
|
+
response = await withRetry(
|
|
612
|
+
({ signal: attemptSignal }) => this.client.chat.completions.create(
|
|
613
|
+
nonStreamParams,
|
|
614
|
+
{ signal: attemptSignal },
|
|
615
|
+
),
|
|
616
|
+
{
|
|
617
|
+
signal,
|
|
618
|
+
maxAttempts: 1,
|
|
619
|
+
// A non-streaming call returns only when generation is done,
|
|
620
|
+
// so the first-byte bound would false-abort it.
|
|
621
|
+
perAttemptTimeoutMs: PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
622
|
+
perAttemptLabel: `${this.name} non-streaming fallback`,
|
|
623
|
+
},
|
|
624
|
+
);
|
|
625
|
+
} catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
const choice = response?.choices?.[0] || null;
|
|
629
|
+
if (!choice) return null;
|
|
630
|
+
const message = choice.message || {};
|
|
631
|
+
let toolCalls;
|
|
632
|
+
try {
|
|
633
|
+
toolCalls = parseToolCalls(choice, this.name);
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
process.stderr.write(
|
|
639
|
+
`[${this.name}] stream failed (${streamErr?.code || streamErr?.message || 'unknown'}); `
|
|
640
|
+
+ `recovered via non-streaming request\n`,
|
|
641
|
+
);
|
|
642
|
+
} catch { /* best-effort */ }
|
|
643
|
+
try {
|
|
644
|
+
appendAgentTrace({
|
|
645
|
+
sessionId: opts?.sessionId || opts?.session?.id || null,
|
|
646
|
+
iteration: Number.isFinite(Number(opts?.iteration)) ? Number(opts.iteration) : null,
|
|
647
|
+
kind: 'transport_fallback',
|
|
648
|
+
provider: this.name,
|
|
649
|
+
model: useModel,
|
|
650
|
+
transport: 'non-streaming',
|
|
651
|
+
payload: {
|
|
652
|
+
from: 'stream',
|
|
653
|
+
to: 'non-streaming',
|
|
654
|
+
reason: streamErr?.retryClassifier || streamErr?.code || streamErr?.message || 'stream_failed',
|
|
655
|
+
error_code: streamErr?.code || null,
|
|
656
|
+
error_http_status: Number(streamErr?.httpStatus || streamErr?.status || 0) || null,
|
|
657
|
+
error_classifier: streamErr?.retryClassifier || streamErr?.midstreamClassifier || null,
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
} catch { /* best-effort */ }
|
|
661
|
+
return {
|
|
662
|
+
response,
|
|
663
|
+
model: response.model || useModel,
|
|
664
|
+
content: typeof message.content === 'string' ? message.content : '',
|
|
665
|
+
toolCalls,
|
|
666
|
+
stopReason: choice.finish_reason || null,
|
|
667
|
+
reasoningContent: typeof message.reasoning_content === 'string' ? message.reasoning_content : null,
|
|
668
|
+
rawUsage: response.usage || null,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
570
672
|
async _doSendXaiResponses(messages, useModel, tools, opts) {
|
|
571
673
|
const signal = opts.signal || null;
|
|
572
674
|
if (signal?.aborted) {
|
|
@@ -589,8 +691,11 @@ export class OpenAICompatProvider {
|
|
|
589
691
|
// existing request bodies.
|
|
590
692
|
store: false,
|
|
591
693
|
include: ['reasoning.encrypted_content'],
|
|
592
|
-
prompt_cache_key: cacheRouting.key,
|
|
593
694
|
};
|
|
695
|
+
// A null routing key means "omit the field" (cache scope 'none'):
|
|
696
|
+
// xAI's own client sends no prompt_cache_key and relies on the
|
|
697
|
+
// service's automatic prefix caching.
|
|
698
|
+
if (cacheRouting.key) params.prompt_cache_key = cacheRouting.key;
|
|
594
699
|
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
595
700
|
const nativeTools = nativeResponsesTools(opts);
|
|
596
701
|
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [], { provider: 'xai' })];
|
|
@@ -797,8 +902,8 @@ export class OpenAICompatProvider {
|
|
|
797
902
|
input,
|
|
798
903
|
store: false,
|
|
799
904
|
include: ['reasoning.encrypted_content'],
|
|
800
|
-
prompt_cache_key: cacheRouting.key,
|
|
801
905
|
};
|
|
906
|
+
if (cacheRouting.key) params.prompt_cache_key = cacheRouting.key;
|
|
802
907
|
const instructions = xaiSystemInstructions(messages);
|
|
803
908
|
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
804
909
|
// xAI rejects instructions together with previous_response_id; the
|
|
@@ -38,6 +38,7 @@ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './ant
|
|
|
38
38
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
39
39
|
import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL, _displayCodexModel } from './openai-oauth.mjs';
|
|
40
40
|
import { createActiveToolItemTracker } from './tool-stream-state.mjs';
|
|
41
|
+
import { parseProviderJsonBatch } from './stream-json-pool.mjs';
|
|
41
42
|
export { envPositiveInt as _envPositiveInt } from './lib/env-utils.mjs';
|
|
42
43
|
|
|
43
44
|
// Public OpenAI Responses API endpoint for the api-key `openai` provider.
|
|
@@ -114,6 +115,18 @@ function _parseSseFrame(frame) {
|
|
|
114
115
|
try { return JSON.parse(raw); } catch { return null; }
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
function _sseJsonPayload(frame) {
|
|
119
|
+
const lines = String(frame || '').split('\n');
|
|
120
|
+
const data = [];
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
if (!line || line.startsWith(':')) continue;
|
|
123
|
+
if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
|
|
124
|
+
}
|
|
125
|
+
if (!data.length) return null;
|
|
126
|
+
const raw = data.join('\n').trim();
|
|
127
|
+
return !raw || raw === '[DONE]' ? null : raw;
|
|
128
|
+
}
|
|
129
|
+
|
|
117
130
|
function _incompleteReasonFromEvent(event) {
|
|
118
131
|
const reasonObj = event?.response?.incomplete_details
|
|
119
132
|
|| event?.incomplete_details
|
|
@@ -1003,10 +1016,12 @@ export async function sendViaHttpSse({
|
|
|
1003
1016
|
buffer += decoder.decode(value, { stream: true });
|
|
1004
1017
|
const parsed = _sseEventsFromBuffer(buffer);
|
|
1005
1018
|
buffer = parsed.rest;
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1019
|
+
const payloads = parsed.frames.map(_sseJsonPayload).filter((payload) => payload !== null);
|
|
1020
|
+
// These bytes have already been delivered by reader.read(). Finish
|
|
1021
|
+
// this bounded chunk before observing abort on the next read so
|
|
1022
|
+
// visible text is never lost at the cancellation boundary.
|
|
1023
|
+
const events = await parseProviderJsonBatch(payloads);
|
|
1024
|
+
for (const event of events) handleEvent(event);
|
|
1010
1025
|
}
|
|
1011
1026
|
// The read() above can unblock via reader.cancel() as {done:true} on an
|
|
1012
1027
|
// external/total-timeout abort. Surface that as the abort/timeout error
|
|
@@ -1014,10 +1029,9 @@ export async function sendViaHttpSse({
|
|
|
1014
1029
|
if (_streamAbortReason) throw _streamAbortReason;
|
|
1015
1030
|
buffer += decoder.decode();
|
|
1016
1031
|
const parsed = _sseEventsFromBuffer(buffer + '\n\n');
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
}
|
|
1032
|
+
const payloads = parsed.frames.map(_sseJsonPayload).filter((payload) => payload !== null);
|
|
1033
|
+
const events = await parseProviderJsonBatch(payloads);
|
|
1034
|
+
for (const event of events) handleEvent(event);
|
|
1021
1035
|
// Flush any partial-sentinel tail held back mid-stream so legitimate
|
|
1022
1036
|
// trailing text is never lost (streamed-text path).
|
|
1023
1037
|
flushLeak();
|
|
@@ -467,6 +467,20 @@ export class OpenAIOAuthProvider {
|
|
|
467
467
|
}
|
|
468
468
|
throw new Error(`OpenAI OAuth token refresh failed (${msg}). Re-authenticate via provider login.`);
|
|
469
469
|
}
|
|
470
|
+
}, {
|
|
471
|
+
// OAuth refresh includes network exchange plus owner-only atomic
|
|
472
|
+
// persistence. Match the Anthropic/xAI lease: the generic 2s file
|
|
473
|
+
// lock timeout is too short under Windows ACL and process contention.
|
|
474
|
+
timeoutMs: 120_000,
|
|
475
|
+
staleMs: 120_000,
|
|
476
|
+
secret: true,
|
|
477
|
+
}, {
|
|
478
|
+
// OAuth refresh includes network exchange plus owner-only atomic
|
|
479
|
+
// persistence. Match the Anthropic/xAI lease: the generic 2s file
|
|
480
|
+
// lock timeout is too short under Windows ACL and process contention.
|
|
481
|
+
timeoutMs: 120_000,
|
|
482
|
+
staleMs: 120_000,
|
|
483
|
+
secret: true,
|
|
470
484
|
}).finally(() => { _oauthRefreshInFlight = null; });
|
|
471
485
|
|
|
472
486
|
this.tokens = await _oauthRefreshInFlight;
|
|
@@ -651,7 +665,7 @@ export class OpenAIOAuthProvider {
|
|
|
651
665
|
// properties (instructions/tools/etc.) but never send the live
|
|
652
666
|
// transcript/user input. The completed generate:false response is
|
|
653
667
|
// retained by the WS transport and anchors the first real request.
|
|
654
|
-
warmupBody: _envFlag('MIXDOG_OPENAI_OAUTH_WS_WARMUP',
|
|
668
|
+
warmupBody: _envFlag('MIXDOG_OPENAI_OAUTH_WS_WARMUP', false)
|
|
655
669
|
? { ...body, input: [], generate: false }
|
|
656
670
|
: null,
|
|
657
671
|
_carriedWarmup: carriedWarmup,
|
|
@@ -113,7 +113,7 @@ export function resolveResponsesTransportPolicy(env = process.env, capabilities
|
|
|
113
113
|
mode,
|
|
114
114
|
requestedMode,
|
|
115
115
|
transport,
|
|
116
|
-
//
|
|
116
|
+
// Reference behavior: default/auto is WS-first but not WS-only. If the
|
|
117
117
|
// websocket path stalls/fails before emitting live output, callers may
|
|
118
118
|
// replay the request over HTTP/SSE. Explicit ws-* modes remain pinned
|
|
119
119
|
// for transport experiments; explicit http-sse bypasses WS entirely.
|
|
@@ -323,10 +323,9 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
|
|
|
323
323
|
'chatgpt-account-id': auth.account_id || '',
|
|
324
324
|
'originator': codexOriginator(),
|
|
325
325
|
'OpenAI-Beta': 'responses_websockets=2026-02-06',
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// login default_client.rs:289-305). The backend fingerprints
|
|
326
|
+
// The reference client merges provider http_headers ("version")
|
|
327
|
+
// plus default headers (User-Agent) into the WS handshake.
|
|
328
|
+
// The backend fingerprints
|
|
330
329
|
// clients on these; missing them can route us onto a different
|
|
331
330
|
// (colder) cache-node class than codex.
|
|
332
331
|
'User-Agent': codexUserAgent(),
|
|
@@ -535,12 +535,9 @@ export async function _streamResponse({
|
|
|
535
535
|
// only) trips a short, named terminal StreamStalledError instead of coasting
|
|
536
536
|
// to the 300s inter-chunk cap / 30-min agent watchdog.
|
|
537
537
|
let semanticIdleTimer = null;
|
|
538
|
-
// WS
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
// request race the TUI 300s watchdog. Keep websocket stalls provider-owned:
|
|
542
|
-
// fail/retry/fallback here first, then let the TUI watchdog remain a final
|
|
543
|
-
// safety net only.
|
|
538
|
+
// WS owns its semantic-idle window. The TUI backstop is deliberately much
|
|
539
|
+
// longer, so this timeout can fail/retry/fallback at the provider boundary
|
|
540
|
+
// without racing an outer turn abort (Claude Code/Codex parity).
|
|
544
541
|
const semanticIdleMs = PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
545
542
|
const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
|
|
546
543
|
// First-meaningful-frame watchdog timer + one-shot latch. Armed alongside
|
|
@@ -35,7 +35,7 @@ let _initChain = Promise.resolve();
|
|
|
35
35
|
let _inFlightPromise = null;
|
|
36
36
|
let _inFlightSig = null;
|
|
37
37
|
let _lastAppliedSig = null;
|
|
38
|
-
// Provider instances are process-global inside the
|
|
38
|
+
// Provider instances are process-global inside the daemon. Catalog
|
|
39
39
|
// readers use this revision to share one raw model snapshot while still
|
|
40
40
|
// rebuilding their cheap route/config projection after auth/config changes.
|
|
41
41
|
let _providerCatalogRevision = 0;
|
|
@@ -367,7 +367,7 @@ function warmupCatalogs() {
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
// Force-refresh each provider's /models catalog
|
|
370
|
+
// Force-refresh each provider's /models catalog once per daemon.
|
|
371
371
|
// lifetime. Every session runtime joins this process-global promise and then
|
|
372
372
|
// reads the same provider-instance caches. Unlike
|
|
373
373
|
// warmupCatalogs (which calls listModels() and so respects the 24h provider
|
|
@@ -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()
|
|
@@ -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) {
|
|
@@ -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
|
])
|