mixdog 0.9.0 → 0.9.1
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/package.json +3 -3
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/src/headless-role.mjs +1 -1
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +2 -2
- package/src/mixdog-session-runtime.mjs +0 -1
- package/src/repl.mjs +0 -2
- package/src/runtime/agent/orchestrator/internal-roles.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +27 -49
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +5 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +8 -27
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +52 -182
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +8 -30
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +258 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/channels/index.mjs +0 -30
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +1 -10
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +3 -24
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +9 -7
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/standalone/seeds.mjs +1 -11
- package/src/tui/App.jsx +35 -12
- package/src/tui/components/PromptInput.jsx +63 -2
- package/src/tui/components/ToolExecution.jsx +7 -2
- package/src/tui/components/tool-output-format.mjs +156 -22
- package/src/tui/components/tool-output-format.test.mjs +93 -1
- package/src/tui/dist/index.mjs +473 -116
- package/src/tui/markdown/format-token.mjs +267 -108
- package/src/tui/markdown/format-token.test.mjs +105 -9
- package/src/tui/theme.mjs +10 -0
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/vendor/ink/build/ink.js +54 -8
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
58
|
"@mozilla/readability": "^0.6.0",
|
|
59
59
|
"chalk": "^5.6.2",
|
|
60
|
+
"cli-highlight": "^2.1.11",
|
|
60
61
|
"diff": "^9.0.0",
|
|
61
62
|
"discord.js": "^14.26.4",
|
|
62
63
|
"ink": "^7.1.0",
|
|
@@ -72,8 +73,7 @@
|
|
|
72
73
|
"undici": "^8.5.0",
|
|
73
74
|
"wrap-ansi": "^10.0.0",
|
|
74
75
|
"ws": "^8.21.0",
|
|
75
|
-
"zod": "^3.25.76"
|
|
76
|
-
"zod-to-json-schema": "^3.25.2"
|
|
76
|
+
"zod": "^3.25.76"
|
|
77
77
|
},
|
|
78
78
|
"overrides": {
|
|
79
79
|
"discord.js": {
|
|
@@ -15,8 +15,8 @@ function assert(condition, message) {
|
|
|
15
15
|
// --- Role normalization ---
|
|
16
16
|
assert(normalizeIngestRole('human') === 'user', 'human should normalize to user');
|
|
17
17
|
assert(normalizeIngestRole('AI') === 'assistant', 'AI should normalize to assistant');
|
|
18
|
-
assert(normalizeIngestRole('tool_result') ===
|
|
19
|
-
assert(normalizeIngestRole('developer') ===
|
|
18
|
+
assert(normalizeIngestRole('tool_result') === null, 'tool_result is not ingested (maps to tool, dropped)');
|
|
19
|
+
assert(normalizeIngestRole('developer') === null, 'developer is not ingested');
|
|
20
20
|
assert(normalizeIngestRole('mystery') === null, 'unknown role should be dropped');
|
|
21
21
|
|
|
22
22
|
// --- Stable source_ref across index/time for untimestamped messages ---
|
package/src/headless-role.mjs
CHANGED
|
@@ -25,7 +25,7 @@ function makeTag(role) {
|
|
|
25
25
|
.replace(/^-+|-+$/g, '');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function buildHeadlessSpawnArgs({ role, tag, cwd, message, provider, model } = {}) {
|
|
29
29
|
const spawnArgs = {
|
|
30
30
|
type: 'spawn',
|
|
31
31
|
role: clean(role),
|
package/src/lib/mixdog-debug.cjs
CHANGED
|
@@ -126,31 +126,9 @@ function appendSessionStartCriticalLog(dataDir, line) {
|
|
|
126
126
|
} catch { /* best-effort */ }
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/**
|
|
130
|
-
* Fallback when call sites omit `{ critical: true }`.
|
|
131
|
-
*/
|
|
132
|
-
function sessionStartCriticalFallback(line) {
|
|
133
|
-
const s = String(line || '');
|
|
134
|
-
if (/reason=ok\b/.test(s)) return false;
|
|
135
|
-
if (/\[session-start\] skip\b/.test(s) && /reason=/.test(s)) return true;
|
|
136
|
-
if (/result=null/.test(s)) return true;
|
|
137
|
-
if (/owner route unavailable/.test(s)) return true;
|
|
138
|
-
if (/memory_port unavailable/.test(s)) return true;
|
|
139
|
-
if (/\baborted\b/i.test(s)) return true;
|
|
140
|
-
if (/\bcycle1\b/.test(s) && /reason=/.test(s) && !/reason=ok\b/.test(s)) return true;
|
|
141
|
-
if (/\b(exception|failed|abort|err=|non-200|missing-dirs|catch endpoint)\b/i.test(s)) {
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
129
|
module.exports = {
|
|
148
|
-
isTruthyEnv,
|
|
149
130
|
isMixdogDebugEnabled,
|
|
150
|
-
isStalePluginLogSibling,
|
|
151
131
|
pruneStalePluginDataLogSiblings,
|
|
152
132
|
appendSessionStartCriticalLog,
|
|
153
|
-
sessionStartCriticalFallback,
|
|
154
133
|
DEFAULT_STALE_LOG_SIBLING_MAX,
|
|
155
|
-
SESSION_START_CRITICAL_LOG,
|
|
156
134
|
};
|
package/src/lib/plugin-paths.cjs
CHANGED
|
@@ -11,18 +11,12 @@
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const os = require('os');
|
|
13
13
|
|
|
14
|
-
const DEFAULT_ROOT = path.join(__dirname, '..');
|
|
15
|
-
|
|
16
14
|
function mixdogHome() {
|
|
17
15
|
return process.env.MIXDOG_HOME || path.join(os.homedir(), '.mixdog');
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
function mixdogRoot() {
|
|
21
|
-
return process.env.MIXDOG_ROOT || DEFAULT_ROOT;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
function resolvePluginData() {
|
|
25
19
|
return process.env.MIXDOG_DATA_DIR || path.join(mixdogHome(), 'data');
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
module.exports = { resolvePluginData
|
|
22
|
+
module.exports = { resolvePluginData };
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* - agent/00-common.md — agent common behavior + universal worker contract (BP2)
|
|
27
27
|
* - agent/10..50-*.md — per-hidden-role bodies (consumed by loadScopedRoleInstructions)
|
|
28
28
|
*
|
|
29
|
-
* Core memory snapshot
|
|
30
|
-
*
|
|
29
|
+
* Core memory snapshot is injected separately from the memory worker (pgdata)
|
|
30
|
+
* (Lead only).
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
const fs = require('fs');
|
|
@@ -2856,7 +2856,6 @@ export async function createMixdogSessionRuntime({
|
|
|
2856
2856
|
// context (see composeSystemPrompt), so there is nothing prompt-side to
|
|
2857
2857
|
// refresh either. `markRefresh`/`changed` are kept only for signature
|
|
2858
2858
|
// compatibility with existing callers.
|
|
2859
|
-
void changed;
|
|
2860
2859
|
void markRefresh;
|
|
2861
2860
|
// Lazy mode: before the first turn (e.g. the initial project-selection
|
|
2862
2861
|
// cwd set), do NOT prewarm — that is exactly the post-first-frame freeze
|
package/src/repl.mjs
CHANGED
|
@@ -59,8 +59,6 @@ async function loadShutdownModule() {
|
|
|
59
59
|
return shutdownModulePromise;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export { printHelp };
|
|
63
|
-
|
|
64
62
|
export async function runRepl({ provider: providerName, model, toolMode = 'full' } = {}) {
|
|
65
63
|
// `--help` short-circuits before any provider init so the smoke test (which
|
|
66
64
|
// invokes `src/cli.mjs --help`) gets clean help output and a 0 exit. We read
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Kind classification:
|
|
23
23
|
* - 'retrieval' : short-lived hidden retrieval roles (explore).
|
|
24
|
-
* - 'maintenance' : background-trigger hidden roles (memory cycle,
|
|
24
|
+
* - 'maintenance' : background-trigger hidden roles (memory cycle, scheduler,
|
|
25
25
|
* webhook). Receive only their own self section in BP2.
|
|
26
26
|
*
|
|
27
27
|
* Permission classification:
|
|
@@ -30,8 +30,11 @@ import {
|
|
|
30
30
|
} from '../stall-policy.mjs';
|
|
31
31
|
import {
|
|
32
32
|
classifyError,
|
|
33
|
+
classifyMidstreamError,
|
|
33
34
|
midstreamBackoffFor,
|
|
35
|
+
MIDSTREAM_RETRY_POLICY,
|
|
34
36
|
retryAfterMsFromError,
|
|
37
|
+
sleepWithAbort,
|
|
35
38
|
withRetry,
|
|
36
39
|
} from './retry-classifier.mjs';
|
|
37
40
|
import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
|
|
@@ -46,8 +49,19 @@ const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
|
46
49
|
// SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
|
|
47
50
|
const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
|
|
48
51
|
|
|
49
|
-
/** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs.
|
|
50
|
-
|
|
52
|
+
/** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs.
|
|
53
|
+
* Sourced from the single shared retry-budget table (MIDSTREAM_RETRY_POLICY.sse). */
|
|
54
|
+
export const ANTHROPIC_MAX_MIDSTREAM_RETRIES = MIDSTREAM_RETRY_POLICY.sse.defaultRetries;
|
|
55
|
+
|
|
56
|
+
// Policy passed to the shared classifyMidstreamError for the SSE path. The
|
|
57
|
+
// top-of-function attempt-budget gate uses defaultRetries (3); perClassifierGate
|
|
58
|
+
// is false so the classifier returns raw bucket strings (the loop owns the
|
|
59
|
+
// MAX_MIDSTREAM_RETRIES bound), matching the former _classifyMidstreamError.
|
|
60
|
+
const SSE_MIDSTREAM_POLICY = {
|
|
61
|
+
mode: 'sse',
|
|
62
|
+
defaultRetries: MIDSTREAM_RETRY_POLICY.sse.defaultRetries,
|
|
63
|
+
perClassifierGate: false,
|
|
64
|
+
};
|
|
51
65
|
|
|
52
66
|
function formatRetryAfter(ms) {
|
|
53
67
|
const n = Number(ms);
|
|
@@ -889,25 +903,10 @@ function _captureMidstreamAbort(state, reason) {
|
|
|
889
903
|
}
|
|
890
904
|
}
|
|
891
905
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
await new Promise((resolve, reject) => {
|
|
899
|
-
const t = setTimeout(() => {
|
|
900
|
-
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
901
|
-
resolve();
|
|
902
|
-
}, ms);
|
|
903
|
-
const onAbort = () => {
|
|
904
|
-
clearTimeout(t);
|
|
905
|
-
const reason = signal.reason;
|
|
906
|
-
reject(reason instanceof Error ? reason : new Error('Anthropic OAuth mid-stream retry backoff aborted'));
|
|
907
|
-
};
|
|
908
|
-
if (signal.aborted) { onAbort(); return; }
|
|
909
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
910
|
-
});
|
|
906
|
+
// Abort-aware mid-stream backoff sleep → shared sleepWithAbort
|
|
907
|
+
// (retry-classifier.mjs). abortMessage preserves the prior fallback text.
|
|
908
|
+
function _midstreamSleepWithAbort(ms, signal) {
|
|
909
|
+
return sleepWithAbort(ms, signal, undefined, 'Anthropic OAuth mid-stream retry backoff aborted');
|
|
911
910
|
}
|
|
912
911
|
|
|
913
912
|
function _statusForAnthropicSseError(type, message) {
|
|
@@ -1258,35 +1257,14 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1258
1257
|
* That keeps recovery limited to transport/stream stalls without risking
|
|
1259
1258
|
* duplicate eager tool execution.
|
|
1260
1259
|
*/
|
|
1260
|
+
// Thin wrapper: the SSE mid-stream decision tree now lives in the shared
|
|
1261
|
+
// classifyMidstreamError (retry-classifier.mjs, policy.mode='sse'). Kept as a
|
|
1262
|
+
// named export so internal call sites AND anthropic.mjs (which imports this
|
|
1263
|
+
// symbol) keep resolving it. Behavior is byte-identical — the shared function
|
|
1264
|
+
// is the relocated original, gated by SSE_MIDSTREAM_POLICY (defaultRetries=3,
|
|
1265
|
+
// perClassifierGate:false).
|
|
1261
1266
|
export function _classifyMidstreamError(err, state) {
|
|
1262
|
-
|
|
1263
|
-
if ((state.attemptIndex | 0) >= ANTHROPIC_MAX_MIDSTREAM_RETRIES) return null;
|
|
1264
|
-
if (state.sawCompleted) return null;
|
|
1265
|
-
if (!state.sawMessageStart) return null;
|
|
1266
|
-
if (state.userAbort) return null;
|
|
1267
|
-
if (state.emittedToolCall) return null;
|
|
1268
|
-
|
|
1269
|
-
if (!err) return null;
|
|
1270
|
-
const status = Number(err?.httpStatus || 0);
|
|
1271
|
-
if (status === 401 || status === 403 || status === 429) return null;
|
|
1272
|
-
|
|
1273
|
-
const name = err?.name || '';
|
|
1274
|
-
if (name === 'AgentStallAbortError') return 'agent_stall';
|
|
1275
|
-
if (name === 'StreamStalledAbortError') return 'stream_stalled';
|
|
1276
|
-
if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall';
|
|
1277
|
-
if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled';
|
|
1278
|
-
|
|
1279
|
-
const code = err?.code || err?.cause?.code || '';
|
|
1280
|
-
if (code === 'ECONNRESET') return 'reset';
|
|
1281
|
-
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout';
|
|
1282
|
-
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns';
|
|
1283
|
-
|
|
1284
|
-
const msg = String(err?.message || '').toLowerCase();
|
|
1285
|
-
if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout';
|
|
1286
|
-
if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated';
|
|
1287
|
-
if (msg.includes('fetch failed')) return 'fetch_failed';
|
|
1288
|
-
|
|
1289
|
-
return null;
|
|
1267
|
+
return classifyMidstreamError(err, state, SSE_MIDSTREAM_POLICY);
|
|
1290
1268
|
}
|
|
1291
1269
|
|
|
1292
1270
|
// --- Build request body ---
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import { loadConfig } from '../config.mjs';
|
|
3
3
|
import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
|
|
4
|
-
import { classifyError, midstreamBackoffFor, withRetry } from './retry-classifier.mjs';
|
|
4
|
+
import { classifyError, midstreamBackoffFor, sleepWithAbort, withRetry } from './retry-classifier.mjs';
|
|
5
5
|
import { traceAgentUsage } from '../agent-trace.mjs';
|
|
6
6
|
import {
|
|
7
7
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
@@ -19,25 +19,10 @@ import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
|
19
19
|
import { enrichModels } from './model-catalog.mjs';
|
|
20
20
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
await new Promise((resolve, reject) => {
|
|
29
|
-
const t = setTimeout(() => {
|
|
30
|
-
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
31
|
-
resolve();
|
|
32
|
-
}, ms);
|
|
33
|
-
const onAbort = () => {
|
|
34
|
-
clearTimeout(t);
|
|
35
|
-
const reason = signal.reason;
|
|
36
|
-
reject(reason instanceof Error ? reason : new Error('Anthropic mid-stream retry backoff aborted'));
|
|
37
|
-
};
|
|
38
|
-
if (signal.aborted) { onAbort(); return; }
|
|
39
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
40
|
-
});
|
|
22
|
+
// Abort-aware mid-stream backoff sleep → shared sleepWithAbort
|
|
23
|
+
// (retry-classifier.mjs). abortMessage preserves the prior fallback text.
|
|
24
|
+
function _midstreamSleepWithAbort(ms, signal) {
|
|
25
|
+
return sleepWithAbort(ms, signal, undefined, 'Anthropic mid-stream retry backoff aborted');
|
|
41
26
|
}
|
|
42
27
|
|
|
43
28
|
// 4-BP cache policy aligned with anthropic-oauth — system + tier3 +
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { loadConfig } from '../config.mjs';
|
|
4
|
-
import { withRetry } from './retry-classifier.mjs';
|
|
4
|
+
import { shouldFallbackTransport, withRetry } from './retry-classifier.mjs';
|
|
5
5
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
6
6
|
import {
|
|
7
7
|
consumeCompatChatCompletionStream,
|
|
@@ -246,33 +246,14 @@ function _envFlag(name, fallback = true) {
|
|
|
246
246
|
return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
// xAI WS→HTTP transport fallback → shared shouldFallbackTransport
|
|
250
|
+
// (retry-classifier.mjs). Identical deny-order + allow-list; the per-provider
|
|
251
|
+
// env flag is computed here and passed via `enabled`.
|
|
249
252
|
function _shouldFallbackXaiWsToHttp(err, signal) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const status = Number(err?.httpStatus || err?.status || 0);
|
|
255
|
-
if (status === 401 || status === 403 || status === 404 || status === 429) return false;
|
|
256
|
-
if (status >= 500 && status < 600) return true;
|
|
257
|
-
const code = String(err?.code || '');
|
|
258
|
-
if (['EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE'].includes(code)) {
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '');
|
|
262
|
-
if ([
|
|
263
|
-
'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
|
|
264
|
-
'first_byte_timeout', 'first_meaningful_timeout',
|
|
265
|
-
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
|
|
266
|
-
'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
|
|
267
|
-
'ws_send_failed',
|
|
268
|
-
].includes(classifier)) {
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
if (/^http_5\d\d$/.test(classifier)) return true;
|
|
272
|
-
if (err?.firstByteTimeout) return true;
|
|
273
|
-
if (err?.firstMeaningfulTimeout) return true;
|
|
274
|
-
const msg = String(err?.message || '');
|
|
275
|
-
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg);
|
|
253
|
+
return shouldFallbackTransport(err, {
|
|
254
|
+
signal,
|
|
255
|
+
enabled: _envFlag('MIXDOG_XAI_WS_HTTP_FALLBACK', true),
|
|
256
|
+
});
|
|
276
257
|
}
|
|
277
258
|
|
|
278
259
|
function useXaiResponsesWebSocketWarmup(opts, config, { previousResponseId, instructions, rawTools }) {
|
|
@@ -33,7 +33,15 @@ import {
|
|
|
33
33
|
traceAgentUsage,
|
|
34
34
|
appendAgentTrace,
|
|
35
35
|
} from '../agent-trace.mjs';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
classifyHandshakeError,
|
|
38
|
+
classifyMidstreamError,
|
|
39
|
+
createStreamSafetyStamps,
|
|
40
|
+
jitterDelayMs,
|
|
41
|
+
MIDSTREAM_RETRY_POLICY,
|
|
42
|
+
populateHttpStatusFromMessage,
|
|
43
|
+
sleepWithAbort,
|
|
44
|
+
} from './retry-classifier.mjs';
|
|
37
45
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
38
46
|
import {
|
|
39
47
|
PROVIDER_RETRY_MAX_ATTEMPTS,
|
|
@@ -71,9 +79,18 @@ const WS_PRE_RESPONSE_CREATED_MS = (() => {
|
|
|
71
79
|
})();
|
|
72
80
|
// Inter-chunk inactivity after first meaningful output.
|
|
73
81
|
const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
// Mid-stream retry budgets + backoff now live in the shared MIDSTREAM_RETRY_POLICY
|
|
83
|
+
// table (retry-classifier.mjs). These aliases keep the local call sites readable
|
|
84
|
+
// and ensure the numbers exist in exactly ONE place.
|
|
85
|
+
const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries;
|
|
86
|
+
const MIDSTREAM_DEFAULT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.defaultRetries;
|
|
87
|
+
const MIDSTREAM_BACKOFF_MS = MIDSTREAM_RETRY_POLICY.ws.backoff;
|
|
88
|
+
// Policy object passed to the shared classifyMidstreamError for the WS path.
|
|
89
|
+
const WS_MIDSTREAM_POLICY = {
|
|
90
|
+
mode: 'ws',
|
|
91
|
+
transientCloseRetries: MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries,
|
|
92
|
+
defaultRetries: MIDSTREAM_RETRY_POLICY.ws.defaultRetries,
|
|
93
|
+
};
|
|
77
94
|
|
|
78
95
|
// Handshake retry policy. The `ws` library surfaces a bare
|
|
79
96
|
// `Opening handshake has timed out` Error after handshakeTimeout; transient
|
|
@@ -1997,34 +2014,12 @@ export async function _streamResponse({
|
|
|
1997
2014
|
* 'http_5xx' (with specific status e.g. 'http_503') — server overload
|
|
1998
2015
|
* null — not retryable
|
|
1999
2016
|
*/
|
|
2017
|
+
// Thin re-export wrapper: handshake classification now lives in the shared
|
|
2018
|
+
// retry-classifier (classifyHandshakeError). Kept here as a named export so
|
|
2019
|
+
// internal call sites (_acquireWithRetry) and any external importer keep
|
|
2020
|
+
// resolving the same symbol.
|
|
2000
2021
|
export function _classifyHandshakeError(err) {
|
|
2001
|
-
|
|
2002
|
-
const code = err.code || '';
|
|
2003
|
-
const msg = String(err.message || '');
|
|
2004
|
-
const status = Number(err.httpStatus || 0);
|
|
2005
|
-
|
|
2006
|
-
// Permanent HTTP (auth / quota / not-found) short-circuits.
|
|
2007
|
-
if (status === 401 || status === 403 || status === 404 || status === 429) {
|
|
2008
|
-
return null;
|
|
2009
|
-
}
|
|
2010
|
-
// 5xx transient.
|
|
2011
|
-
if (status >= 500 && status < 600) {
|
|
2012
|
-
return `http_${status}`;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
// Node errno codes.
|
|
2016
|
-
if (code === 'ECONNRESET') return 'reset';
|
|
2017
|
-
if (code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA') return 'dns';
|
|
2018
|
-
if (code === 'ECONNREFUSED') return 'refused';
|
|
2019
|
-
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout';
|
|
2020
|
-
if (code === 'EWSACQUIRETIMEOUT') return 'acquire_timeout';
|
|
2021
|
-
if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'EPIPE') return 'network';
|
|
2022
|
-
|
|
2023
|
-
// `ws` library's handshake-timeout path: thrown as a bare Error.
|
|
2024
|
-
if (/opening handshake has timed out/i.test(msg)) return 'timeout';
|
|
2025
|
-
if (/socket hang up/i.test(msg)) return 'reset';
|
|
2026
|
-
|
|
2027
|
-
return null;
|
|
2022
|
+
return classifyHandshakeError(err);
|
|
2028
2023
|
}
|
|
2029
2024
|
|
|
2030
2025
|
/**
|
|
@@ -2065,128 +2060,25 @@ export function _classifyHandshakeError(err) {
|
|
|
2065
2060
|
* - HTTP 401 / 403 / 429 surfaced on the error
|
|
2066
2061
|
* - state.attemptIndex has reached the classifier-specific retry budget
|
|
2067
2062
|
*/
|
|
2063
|
+
// Thin wrapper: the full WS mid-stream decision tree now lives in the shared
|
|
2064
|
+
// classifyMidstreamError (retry-classifier.mjs, policy.mode='ws'). Kept as a
|
|
2065
|
+
// named export so internal call sites and any external importer keep resolving
|
|
2066
|
+
// the same symbol. Behavior is byte-identical — the shared function is the
|
|
2067
|
+
// relocated original, with the per-classifier budget gating supplied by
|
|
2068
|
+
// WS_MIDSTREAM_POLICY (transientCloseRetries=4, defaultRetries=2).
|
|
2068
2069
|
export function _classifyMidstreamError(err, state) {
|
|
2069
|
-
|
|
2070
|
-
const attemptIndex = state.attemptIndex | 0;
|
|
2071
|
-
// Already completed (shouldn't throw, but defensive).
|
|
2072
|
-
if (state.sawCompleted) return null;
|
|
2073
|
-
// Any tool call already surfaced to the caller — retrying would
|
|
2074
|
-
// normally duplicate the side effect. EXCEPTION: ws_1000 truncation
|
|
2075
|
-
// (server-side normal close after response.created, before completion)
|
|
2076
|
-
// leaves the caller with an orphaned tool_use that the next turn cannot
|
|
2077
|
-
// pair to a tool_result, which the provider rejects with a hard 400.
|
|
2078
|
-
// The duplicate-side-effect risk is preferable to deterministic worker
|
|
2079
|
-
// death, especially for detached agents that re-dispatch idempotently.
|
|
2080
|
-
if (state.emittedToolCall) {
|
|
2081
|
-
const _cc = Number(err?.wsCloseCode || state.wsCloseCode || 0);
|
|
2082
|
-
if (!(_cc === 1000 && state.sawResponseCreated && !state.sawCompleted)) return null;
|
|
2083
|
-
}
|
|
2084
|
-
// Live-text invariant: once a non-empty text chunk has been relayed to the
|
|
2085
|
-
// client (gateway live mode), the rendered output cannot be withdrawn and a
|
|
2086
|
-
// retry would concatenate a second attempt. Treat every subsequent
|
|
2087
|
-
// mid-stream/truncated failure as final — never retry.
|
|
2088
|
-
if (state.emittedText || err?.liveTextEmitted) return null;
|
|
2089
|
-
// Post-upgrade-no-first-event: the socket opened, our response.create
|
|
2090
|
-
// frame was sent, but the server never emitted a single event before
|
|
2091
|
-
// the short pre-`response.created` watchdog fired. The handshake retry
|
|
2092
|
-
// layer only sees pre-upgrade failures and the legacy pre-stream gate
|
|
2093
|
-
// below would deny this case (sawResponseCreated === false). Tag it
|
|
2094
|
-
// here as a fast retryable bucket so the worker reconnects within
|
|
2095
|
-
// seconds instead of stalling for the full first-meaningful window.
|
|
2096
|
-
if (state.firstByteTimeout || err?.firstByteTimeout) {
|
|
2097
|
-
return _allowMidstreamRetry('first_byte_timeout', attemptIndex);
|
|
2098
|
-
}
|
|
2099
|
-
if (state.firstMeaningfulTimeout || err?.firstMeaningfulTimeout) {
|
|
2100
|
-
return _allowMidstreamRetry('first_meaningful_timeout', attemptIndex);
|
|
2101
|
-
}
|
|
2102
|
-
// _sendFrame failure (socket not OPEN, send callback errored, JSON
|
|
2103
|
-
// serialize threw). Always retryable: caller will forceFresh next
|
|
2104
|
-
// attempt so the wedged socket is dropped.
|
|
2105
|
-
if (err?.wsSendFailed || state.wsSendFailed) {
|
|
2106
|
-
return _allowMidstreamRetry('ws_send_failed', attemptIndex);
|
|
2107
|
-
}
|
|
2108
|
-
// Pre-stream failures normally belong to the handshake retry layer. BUT
|
|
2109
|
-
// WS close 1011 / 1012 can fire after the 101 upgrade but BEFORE the
|
|
2110
|
-
// first response.created event when the server's keepalive times out or
|
|
2111
|
-
// the service restarts. Neither the handshake retry layer (it only sees
|
|
2112
|
-
// pre-upgrade failures) nor the existing mid-stream gate covers this
|
|
2113
|
-
// window, so permit bounded retry here for those two codes only.
|
|
2114
|
-
if (!state.sawResponseCreated) {
|
|
2115
|
-
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0);
|
|
2116
|
-
if (closeCode !== 1011 && closeCode !== 1012) return null;
|
|
2117
|
-
}
|
|
2118
|
-
// User/caller abort — never retry.
|
|
2119
|
-
if (state.userAbort) return null;
|
|
2120
|
-
|
|
2121
|
-
if (!err) return null;
|
|
2122
|
-
const status = Number(err?.httpStatus || 0);
|
|
2123
|
-
if (status === 401 || status === 403 || status === 429) return null;
|
|
2124
|
-
// Transient 5xx surfaced via populateHttpStatusFromMessage (case 'error'
|
|
2125
|
-
// and case 'response.failed' branches sniff server-supplied text like
|
|
2126
|
-
// "Our servers are currently overloaded" and assign httpStatus=503).
|
|
2127
|
-
// Allow one bounded mid-stream retry on the same budget as the WS close-
|
|
2128
|
-
// code buckets above so server-side overload no longer leaks straight
|
|
2129
|
-
// to the caller without a single retry attempt.
|
|
2130
|
-
if (status >= 500 && status < 600) {
|
|
2131
|
-
return _allowMidstreamRetry(`http_${status}`, attemptIndex);
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
const name = err?.name || '';
|
|
2135
|
-
if (name === 'AgentStallAbortError') return _allowMidstreamRetry('agent_stall', attemptIndex);
|
|
2136
|
-
if (name === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
|
|
2137
|
-
|
|
2138
|
-
// Watchdog abort surfaced via externalSignal handler → err is the reason
|
|
2139
|
-
// itself. state.watchdogAbort captures the class name when the error
|
|
2140
|
-
// shape was preserved but the name was stripped by some wrapper.
|
|
2141
|
-
if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstreamRetry('agent_stall', attemptIndex);
|
|
2142
|
-
if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
|
|
2143
|
-
|
|
2144
|
-
// WS close codes: prefer the decorated property, fall back to state.
|
|
2145
|
-
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0);
|
|
2146
|
-
if (closeCode === 1006) return _allowMidstreamRetry('ws_1006', attemptIndex);
|
|
2147
|
-
if (closeCode === 1011) return _allowMidstreamRetry('ws_1011', attemptIndex);
|
|
2148
|
-
if (closeCode === 1012) return _allowMidstreamRetry('ws_1012', attemptIndex);
|
|
2149
|
-
// Private 4xxx codes from a server/proxy are auth/policy/application closes;
|
|
2150
|
-
// never treat them as transient. 4000 is our local pre-stream watchdog code.
|
|
2151
|
-
if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null;
|
|
2152
|
-
if (closeCode === 4000) return _allowMidstreamRetry('ws_4000', attemptIndex);
|
|
2153
|
-
// Server-side normal close (1000) AFTER response.created but BEFORE
|
|
2154
|
-
// response.completed = truncated stream; legitimate transient. The
|
|
2155
|
-
// pre-stream gate above already rejects 1000 before sawResponseCreated
|
|
2156
|
-
// (handshake retry layer owns that window).
|
|
2157
|
-
if (closeCode === 1000 && state.sawResponseCreated && !state.sawCompleted) return _allowMidstreamRetry('ws_1000', attemptIndex);
|
|
2158
|
-
|
|
2159
|
-
// response.failed payload mentioning network_error / stream_disconnected.
|
|
2160
|
-
// xAI's gRPC backend periodically rotates auth context (server-side TTL)
|
|
2161
|
-
// and surfaces "Auth context expired" as a response.failed event. The
|
|
2162
|
-
// attemptIndex > 0 path in sendViaWebSocket forces a fresh WS handshake,
|
|
2163
|
-
// which re-authenticates — so a single bounded retry recovers the turn
|
|
2164
|
-
// instead of letting the worker die mid-session.
|
|
2165
|
-
const failed = err?.responseFailed || state.responseFailedPayload;
|
|
2166
|
-
if (failed) {
|
|
2167
|
-
try {
|
|
2168
|
-
const blob = JSON.stringify(failed).toLowerCase();
|
|
2169
|
-
if (blob.includes('stream_disconnected')) return _allowMidstreamRetry('response_failed_disconnected', attemptIndex);
|
|
2170
|
-
if (blob.includes('network_error')) return _allowMidstreamRetry('response_failed_network', attemptIndex);
|
|
2171
|
-
if (blob.includes('auth context expired')) return _allowMidstreamRetry('response_failed_auth_expired', attemptIndex);
|
|
2172
|
-
} catch {}
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
// Unknown → default-deny (don't risk a second full-cost turn for an error
|
|
2176
|
-
// class we haven't proven is transient).
|
|
2177
|
-
return null;
|
|
2070
|
+
return classifyMidstreamError(err, state, WS_MIDSTREAM_POLICY);
|
|
2178
2071
|
}
|
|
2179
2072
|
|
|
2073
|
+
// Per-classifier retry budget, used by the sendViaWebSocket loop to bound the
|
|
2074
|
+
// attempt count once classifyMidstreamError returns a bucket. Mirrors the
|
|
2075
|
+
// shared _midstreamLimitFor(ws) — the numbers come from MIDSTREAM_RETRY_POLICY.
|
|
2180
2076
|
function _midstreamRetryLimit(classifier) {
|
|
2181
2077
|
return classifier === 'ws_1006' || classifier === 'ws_1011'
|
|
2182
2078
|
? MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT
|
|
2183
2079
|
: MIDSTREAM_DEFAULT_RETRY_LIMIT;
|
|
2184
2080
|
}
|
|
2185
2081
|
|
|
2186
|
-
function _allowMidstreamRetry(classifier, attemptIndex) {
|
|
2187
|
-
return attemptIndex < _midstreamRetryLimit(classifier) ? classifier : null;
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
2082
|
function _midstreamBackoffFor(retryNumber) {
|
|
2191
2083
|
const raw = MIDSTREAM_BACKOFF_MS[Math.min(Math.max(retryNumber, 1), MIDSTREAM_BACKOFF_MS.length) - 1];
|
|
2192
2084
|
return jitterDelayMs(raw);
|
|
@@ -2200,25 +2092,11 @@ function _backoffFor(attempt) {
|
|
|
2200
2092
|
|
|
2201
2093
|
const _defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2202
2094
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2209
|
-
await new Promise((resolve, reject) => {
|
|
2210
|
-
const t = setTimeout(() => {
|
|
2211
|
-
externalSignal.removeEventListener('abort', onAbort);
|
|
2212
|
-
resolve();
|
|
2213
|
-
}, ms);
|
|
2214
|
-
const onAbort = () => {
|
|
2215
|
-
clearTimeout(t);
|
|
2216
|
-
const reason = externalSignal.reason;
|
|
2217
|
-
reject(reason instanceof Error ? reason : new Error('OpenAI OAuth WS retry backoff aborted'));
|
|
2218
|
-
};
|
|
2219
|
-
if (externalSignal.aborted) { onAbort(); return; }
|
|
2220
|
-
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
2221
|
-
});
|
|
2095
|
+
// Abort-aware backoff sleep → shared sleepWithAbort (retry-classifier.mjs). The
|
|
2096
|
+
// abortMessage preserves the prior fallback text when the abort reason is not an
|
|
2097
|
+
// Error; _sleepFn (test seam) is threaded through as the no-signal sleep impl.
|
|
2098
|
+
function _sleepWithAbort(ms, externalSignal, sleepFn = _defaultSleep) {
|
|
2099
|
+
return sleepWithAbort(ms, externalSignal, sleepFn, 'OpenAI OAuth WS retry backoff aborted');
|
|
2222
2100
|
}
|
|
2223
2101
|
|
|
2224
2102
|
/**
|
|
@@ -2379,23 +2257,15 @@ export async function sendViaWebSocket({
|
|
|
2379
2257
|
// already-rendered output. A text-emitting attempt is never retry-eligible
|
|
2380
2258
|
// (_classifyMidstreamError returns null on emittedText), so the surfaced
|
|
2381
2259
|
// error is frequently an EARLIER attempt's firstAttemptError that never saw
|
|
2382
|
-
// the marker;
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
// call was forwarded, surfaced errors must block HTTP fallback / reissue.
|
|
2392
|
-
let toolEmittedAcrossAttempts = false;
|
|
2393
|
-
const _stampTool = (e) => {
|
|
2394
|
-
if (toolEmittedAcrossAttempts && e) {
|
|
2395
|
-
try { e.emittedToolCall = true; e.unsafeToRetry = true; } catch {}
|
|
2396
|
-
}
|
|
2397
|
-
return e;
|
|
2398
|
-
};
|
|
2260
|
+
// the marker; stampText re-applies it on every throw path. The latch state
|
|
2261
|
+
// + stamp semantics now come from the shared createStreamSafetyStamps()
|
|
2262
|
+
// factory (retry-classifier.mjs) — identical to the former _stampLiveText /
|
|
2263
|
+
// _stampTool closures. markText()/markTool() set the latch (replacing the
|
|
2264
|
+
// liveTextEmittedAcrossAttempts / toolEmittedAcrossAttempts booleans);
|
|
2265
|
+
// stampText/stampTool re-apply the markers on every throw.
|
|
2266
|
+
const _safetyStamps = createStreamSafetyStamps();
|
|
2267
|
+
const _stampLiveText = _safetyStamps.stampText;
|
|
2268
|
+
const _stampTool = _safetyStamps.stampTool;
|
|
2399
2269
|
// Server-side xAI conversation anchor preserved across mid-stream
|
|
2400
2270
|
// retries. xAI keys its conversation by previous_response_id alone
|
|
2401
2271
|
// (sessionToken is null for xAI in _mintSessionToken); a forceFresh
|
|
@@ -2665,10 +2535,10 @@ export async function sendViaWebSocket({
|
|
|
2665
2535
|
// Latch across attempts: even though THIS error is never
|
|
2666
2536
|
// retry-eligible once text is out, a later/earlier surfaced
|
|
2667
2537
|
// error (firstAttemptError) must still carry the marker.
|
|
2668
|
-
|
|
2538
|
+
_safetyStamps.markText();
|
|
2669
2539
|
}
|
|
2670
2540
|
if (midState.emittedToolCall) {
|
|
2671
|
-
|
|
2541
|
+
_safetyStamps.markTool();
|
|
2672
2542
|
}
|
|
2673
2543
|
_stampLiveText(err);
|
|
2674
2544
|
_stampTool(err);
|