mixdog 0.9.87 → 0.9.89
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 +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
* Single source of truth across every provider (openai-oauth-ws, openai-oauth,
|
|
5
5
|
* anthropic-oauth, anthropic, gemini, openai-ws, openai-compat).
|
|
6
6
|
*
|
|
7
|
-
* Goal: when a provider returns a transient server-side condition we
|
|
8
|
-
* retry; when it returns a deterministic refusal (auth, permission,
|
|
9
|
-
* we should fail fast.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Goal: when a provider returns a TYPED transient server-side condition we
|
|
8
|
+
* should retry; when it returns a deterministic refusal (auth, permission,
|
|
9
|
+
* quota) we should fail fast. Evidence is structural only — HTTP status,
|
|
10
|
+
* Node errno, SDK error type, WS close code, or a typed field on a wire event.
|
|
11
|
+
* Error MESSAGE TEXT is never parsed into a status/transience/auth verdict:
|
|
12
|
+
* an untyped failure stays 'unknown' and is surfaced, not retried.
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
|
-
* import { classifyError,
|
|
15
|
+
* import { classifyError, typedStatusFrom } from './retry-classifier.mjs'
|
|
16
16
|
* const kind = classifyError(err) // 'auth' | 'permanent' | 'transient' | 'unknown'
|
|
17
|
-
*
|
|
17
|
+
* typedStatusFrom(event.response?.error, event) // structured status, or 0
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import {
|
|
@@ -24,6 +24,9 @@ import {
|
|
|
24
24
|
PROVIDER_RETRY_MAX_ATTEMPTS,
|
|
25
25
|
createTimeoutSignal,
|
|
26
26
|
} from '../stall-policy.mjs'
|
|
27
|
+
import { readStreamOutcome } from './lib/stream-outcome.mjs'
|
|
28
|
+
|
|
29
|
+
export { readStreamOutcome, stampStreamOutcome, isReplaySafe, isReplayUnsafe, canPromoteToSuccess, hasObservedOutput, hasDispatchedToolCalls, STREAM_TRANSPORTS } from './lib/stream-outcome.mjs'
|
|
27
30
|
|
|
28
31
|
// HTTP statuses considered transient — safe to retry with backoff.
|
|
29
32
|
// 408 — request timeout
|
|
@@ -39,47 +42,23 @@ const TRANSIENT_STATUSES = new Set([408, 409])
|
|
|
39
42
|
const AUTH_STATUSES = new Set([401, 403])
|
|
40
43
|
const PERMANENT_STATUSES = new Set([400, 404, 405, 410, 415, 422])
|
|
41
44
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// message text plus a request ID, with no HTTP status on the WS event.
|
|
48
|
-
{ regex: /(?:an error occurred while processing your request|please include the request id)/i, status: 503 },
|
|
49
|
-
// Overload / transient 5xx — server is asking us to back off. The `\b`
|
|
50
|
-
// anchor is intentionally OMITTED on the trailing side of `overload` so
|
|
51
|
-
// "overloaded" / "overloading" both match (inflected forms are common in
|
|
52
|
-
// server error text).
|
|
53
|
-
{ regex: /(?:overload(?:ed|ing)?|temporarily unavailable|try again later|service unavailable|bad gateway|gateway timeout)/i, status: 503 },
|
|
54
|
-
// Explicit 5xx mention.
|
|
55
|
-
{ regex: /\b(?:5\d\d|http 5\d\d)\b/i, status: 503 },
|
|
56
|
-
// Rate limit / quota — same retry posture as 429 but treat as permanent
|
|
57
|
-
// by classifyError because per-call retry won't change the answer (the
|
|
58
|
-
// window must elapse). Providers that want time-bounded retry should
|
|
59
|
-
// honor Retry-After on the original response, not loop here.
|
|
60
|
-
{ regex: /(?:rate[_ -]?limit|quota|too many requests|resource exhausted|insufficient_quota|quota_exceeded)/i, status: 429 },
|
|
61
|
-
// Auth — never retryable from our side.
|
|
62
|
-
{ regex: /\b(?:unauthorized|unauthorised|authentication|not authenticated|token expired|access token|invalid api key)\b/i, status: 401 },
|
|
63
|
-
{ regex: /\b(?:forbidden|permission denied|policy violation)\b/i, status: 403 },
|
|
64
|
-
]
|
|
45
|
+
// Structured status fields a provider error / wire event may carry. A value is
|
|
46
|
+
// accepted only when it is a real numeric HTTP status; string codes
|
|
47
|
+
// ('server_error', 'forbidden', ...) and free text are ignored, so nothing is
|
|
48
|
+
// ever synthesized from a message.
|
|
49
|
+
const TYPED_STATUS_KEYS = ['httpStatus', 'http_status', 'status', 'statusCode', 'status_code', 'code']
|
|
65
50
|
|
|
66
51
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* Returns the resolved httpStatus (existing or newly assigned), or 0 when
|
|
72
|
-
* nothing matched.
|
|
52
|
+
* Read the first TYPED HTTP status carried by any of `sources` (error object,
|
|
53
|
+
* wire event, event.response.error payload, ...). Returns 0 when none of them
|
|
54
|
+
* declares one. Never inspects message text.
|
|
73
55
|
*/
|
|
74
|
-
export function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (regex.test(text)) {
|
|
81
|
-
err.httpStatus = status
|
|
82
|
-
return status
|
|
56
|
+
export function typedStatusFrom(...sources) {
|
|
57
|
+
for (const source of sources) {
|
|
58
|
+
if (!source || typeof source !== 'object') continue
|
|
59
|
+
for (const key of TYPED_STATUS_KEYS) {
|
|
60
|
+
const n = Number(source[key])
|
|
61
|
+
if (Number.isFinite(n) && n >= 100 && n <= 599) return Math.floor(n)
|
|
83
62
|
}
|
|
84
63
|
}
|
|
85
64
|
return 0
|
|
@@ -98,12 +77,13 @@ export function populateHttpStatusFromMessage(err, msg = null) {
|
|
|
98
77
|
*/
|
|
99
78
|
export function classifyError(err) {
|
|
100
79
|
if (!err) return 'unknown'
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
80
|
+
// Canonical stream-outcome contract owns replay safety: exposed
|
|
81
|
+
// text/reasoning or a dispatched/ambiguous complete tool call makes
|
|
82
|
+
// re-issuing the same turn unsafe (double side effect / output
|
|
83
|
+
// concatenation) → 'permanent'. Everything else stays classifiable by the
|
|
84
|
+
// typed rules below; an unknown/untyped failure ends as 'unknown', never a
|
|
85
|
+
// blanket retry.
|
|
86
|
+
if (readStreamOutcome(err).replayUnsafe === true) return 'permanent'
|
|
107
87
|
// Cancellation is a caller decision, never a transport symptom. Anthropic's
|
|
108
88
|
// APIUserAbortError inherits Error without overriding `name`, so recognize
|
|
109
89
|
// only exact SDK constructor/type markers (plus standard AbortError markers)
|
|
@@ -112,7 +92,7 @@ export function classifyError(err) {
|
|
|
112
92
|
if (chain.some(isExplicitUserAbortError)) return 'permanent'
|
|
113
93
|
|
|
114
94
|
// Current typed HTTP status outranks stale stream/connection annotations.
|
|
115
|
-
const status = Number(err.httpStatus || err.status || err.response?.status || 0) ||
|
|
95
|
+
const status = Number(err.httpStatus || err.status || err.response?.status || 0) || 0
|
|
116
96
|
if (AUTH_STATUSES.has(status)) return 'auth'
|
|
117
97
|
if (status === 429) return 'permanent'
|
|
118
98
|
if (PERMANENT_STATUSES.has(status)
|
|
@@ -129,14 +109,10 @@ export function classifyError(err) {
|
|
|
129
109
|
// transient: we can retry the same request and may succeed.
|
|
130
110
|
if (chain.some((item) => TRANSIENT_ERROR_CODES.has(String(item?.code || '')))) return 'transient'
|
|
131
111
|
// The Anthropic SDK uses APIConnectionError for transport failures which
|
|
132
|
-
// may not carry a Node errno. Native fetch
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (name === 'APIConnectionError'
|
|
137
|
-
|| (name === 'TypeError' && /fetch failed|network error/i.test(message))) {
|
|
138
|
-
return 'transient'
|
|
139
|
-
}
|
|
112
|
+
// may not carry a Node errno. Native fetch wraps its errno in cause.code,
|
|
113
|
+
// which the bounded chain check above already covers — a bare
|
|
114
|
+
// TypeError('fetch failed') with no errno stays 'unknown'.
|
|
115
|
+
if (String(err.name || '') === 'APIConnectionError') return 'transient'
|
|
140
116
|
|
|
141
117
|
return 'unknown'
|
|
142
118
|
}
|
|
@@ -261,8 +237,6 @@ function isPermanentQuotaError(err) {
|
|
|
261
237
|
for (const item of boundedCauseChain(err)) {
|
|
262
238
|
const codes = [item?.code, item?.error?.code]
|
|
263
239
|
if (codes.some((code) => permanentCodes.has(String(code || '').toLowerCase()))) return true
|
|
264
|
-
const text = `${String(item?.message || '')} ${String(item?.error?.message || '')}`
|
|
265
|
-
if (/insufficient_quota|quota[_ -]?exceeded|resource exhausted/i.test(text)) return true
|
|
266
240
|
}
|
|
267
241
|
return false
|
|
268
242
|
}
|
|
@@ -389,12 +363,13 @@ export function classifyMidstreamError(err, signals, policy = {}) {
|
|
|
389
363
|
|
|
390
364
|
// WebSocket classification consumes the provider's stream-state signals.
|
|
391
365
|
function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
392
|
-
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
if (
|
|
366
|
+
// Terminal + replay gate. A mid-stream retry re-issues the turn, so it is
|
|
367
|
+
// denied once visible output was relayed or a tool call was dispatched
|
|
368
|
+
// (including a nominal close-1000 before response.completed: the tool may be
|
|
369
|
+
// executing). Everything below is the typed transient classification.
|
|
370
|
+
const outcome = readStreamOutcome(err, state)
|
|
371
|
+
if (outcome.terminalObserved) return null
|
|
372
|
+
if (outcome.replaySafe !== true) return null
|
|
398
373
|
if (err?.wsFrameTooLarge || state.wsFrameTooLarge) {
|
|
399
374
|
return _allowMidstream('ws_frame_too_large', attemptIndex, policy)
|
|
400
375
|
}
|
|
@@ -418,8 +393,7 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
418
393
|
if (name === 'StreamStalledAbortError' || name === 'StreamStalledError'
|
|
419
394
|
|| err?.code === 'ESTREAMSTALL' || err?.streamStalled === true
|
|
420
395
|
|| state.watchdogAbort === 'StreamStalledAbortError') {
|
|
421
|
-
// A stall
|
|
422
|
-
if (err?.unsafeToRetry === true) return null
|
|
396
|
+
// A stall after any exposure was already denied by the outcome gate.
|
|
423
397
|
return _allowMidstream('stream_stalled', attemptIndex, policy)
|
|
424
398
|
}
|
|
425
399
|
if (closeCode === 4000) return _allowMidstream('ws_4000', attemptIndex, policy)
|
|
@@ -444,9 +418,6 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
444
418
|
const name = err?.name || ''
|
|
445
419
|
if (name === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
|
|
446
420
|
if (name === 'StreamStalledAbortError' || name === 'StreamStalledError' || err?.code === 'ESTREAMSTALL' || err?.streamStalled === true) {
|
|
447
|
-
// A stall that fired AFTER a tool call was emitted is unsafe to replay
|
|
448
|
-
// (double side-effect); surface it as terminal so the turn is not retried.
|
|
449
|
-
if (err?.unsafeToRetry === true) return null
|
|
450
421
|
return _allowMidstream('stream_stalled', attemptIndex, policy)
|
|
451
422
|
}
|
|
452
423
|
|
|
@@ -463,23 +434,42 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
463
434
|
|
|
464
435
|
const failed = err?.responseFailed || state.responseFailedPayload
|
|
465
436
|
if (failed) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
437
|
+
// STRUCTURED evidence only: the failure's own numeric status and its
|
|
438
|
+
// explicit error code/type field. The payload is never stringified and
|
|
439
|
+
// searched — a message/body that merely CONTAINS "network_error",
|
|
440
|
+
// "stream_disconnected" or "auth context expired" is not a typed retry
|
|
441
|
+
// signal and leaves the turn terminal.
|
|
442
|
+
const detail = failed?.response?.error || failed?.error || failed
|
|
443
|
+
const failedStatus = typedStatusFrom(detail, failed?.response, failed)
|
|
444
|
+
if (failedStatus >= 500 && failedStatus < 600) {
|
|
445
|
+
return _allowMidstream(`http_${failedStatus}`, attemptIndex, policy)
|
|
446
|
+
}
|
|
447
|
+
for (const field of [detail?.code, detail?.type]) {
|
|
448
|
+
const key = typeof field === 'string' ? field.trim().toLowerCase() : ''
|
|
449
|
+
const classifier = RESPONSE_FAILED_CODE_CLASSIFIERS.get(key)
|
|
450
|
+
if (classifier) return _allowMidstream(classifier, attemptIndex, policy)
|
|
451
|
+
}
|
|
472
452
|
}
|
|
473
453
|
|
|
474
454
|
return null
|
|
475
455
|
}
|
|
476
456
|
|
|
457
|
+
// Explicit `response.failed` error codes/types that describe a transport-level
|
|
458
|
+
// interruption (Codex retries these); every other code is terminal.
|
|
459
|
+
const RESPONSE_FAILED_CODE_CLASSIFIERS = new Map([
|
|
460
|
+
['stream_disconnected', 'response_failed_disconnected'],
|
|
461
|
+
['network_error', 'response_failed_network'],
|
|
462
|
+
['auth_context_expired', 'response_failed_auth_expired'],
|
|
463
|
+
['auth_expired', 'response_failed_auth_expired'],
|
|
464
|
+
])
|
|
465
|
+
|
|
477
466
|
// SSE classification consumes the provider's stream-state signals.
|
|
478
467
|
function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
479
468
|
if (attemptIndex >= policy.defaultRetries) return null
|
|
480
|
-
|
|
469
|
+
const outcome = readStreamOutcome(err, state)
|
|
470
|
+
if (outcome.terminalObserved) return null
|
|
481
471
|
if (state.userAbort) return null
|
|
482
|
-
if (
|
|
472
|
+
if (outcome.replaySafe !== true) return null
|
|
483
473
|
|
|
484
474
|
if (!err) return null
|
|
485
475
|
const status = Number(err?.httpStatus || err?.status || err?.response?.status || 0)
|
|
@@ -490,9 +480,6 @@ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
|
490
480
|
const name = err?.name || ''
|
|
491
481
|
if (name === 'AgentStallAbortError') return 'agent_stall'
|
|
492
482
|
if (name === 'StreamStalledAbortError' || name === 'StreamStalledError' || err?.code === 'ESTREAMSTALL' || err?.streamStalled === true) {
|
|
493
|
-
// A stall AFTER a tool emit is unsafe to replay (double side-effect) →
|
|
494
|
-
// terminal (null), no mid-stream retry. Otherwise route to stream_stalled.
|
|
495
|
-
if (err?.unsafeToRetry === true) return null
|
|
496
483
|
return 'stream_stalled'
|
|
497
484
|
}
|
|
498
485
|
if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall'
|
|
@@ -503,10 +490,6 @@ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
|
503
490
|
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
|
|
504
491
|
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns'
|
|
505
492
|
|
|
506
|
-
const msg = String(err?.message || '').toLowerCase()
|
|
507
|
-
if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout'
|
|
508
|
-
if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated'
|
|
509
|
-
if (msg.includes('fetch failed')) return 'fetch_failed'
|
|
510
493
|
if (classifyError(err) === 'transient') return 'connection'
|
|
511
494
|
|
|
512
495
|
return null
|
|
@@ -530,23 +513,25 @@ const TRANSPORT_FALLBACK_ERRNO = new Set([
|
|
|
530
513
|
export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
|
|
531
514
|
if (!enabled) return false
|
|
532
515
|
if (signal?.aborted) return false
|
|
533
|
-
|
|
534
|
-
|
|
516
|
+
// Transport fallback re-issues the request on another transport: it is a
|
|
517
|
+
// replay, so the exposure deny applies. Eligibility itself stays typed
|
|
518
|
+
// (status / errno / classifier), matching Codex's WS→HTTPS switch.
|
|
519
|
+
if (readStreamOutcome(err).replaySafe !== true) return false
|
|
535
520
|
const status = Number(err?.httpStatus || err?.status || 0)
|
|
536
|
-
// 401 is auth recovery, never transport fallback.
|
|
537
|
-
//
|
|
538
|
-
//
|
|
521
|
+
// 401 is auth recovery, never transport fallback. 426 is the explicit
|
|
522
|
+
// immediate WS→HTTPS switch; every other status must be TYPED transient
|
|
523
|
+
// (408/409/5xx) — an arbitrary nonzero status is not fallback evidence.
|
|
539
524
|
if (status === 401) return false
|
|
540
525
|
if (status === 426) return true
|
|
541
|
-
if (status
|
|
526
|
+
if (TRANSIENT_STATUSES.has(status) || (status >= 500 && status < 600)) return true
|
|
527
|
+
if (status > 0) return false
|
|
542
528
|
const code = String(err?.code || '')
|
|
543
529
|
if (TRANSPORT_FALLBACK_ERRNO.has(code)) return true
|
|
544
530
|
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '')
|
|
545
531
|
if (TRANSPORT_FALLBACK_CLASSIFIERS.has(classifier)) return true
|
|
546
532
|
if (/^http_5\d\d$/.test(classifier)) return true
|
|
547
533
|
if (err?.firstByteTimeout) return true
|
|
548
|
-
|
|
549
|
-
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg)
|
|
534
|
+
return false
|
|
550
535
|
}
|
|
551
536
|
|
|
552
537
|
// C) Stream-safety stamp latches. Mirrors openai-oauth-ws's _stampLiveText /
|
|
@@ -645,12 +630,17 @@ function _sleepChunkWithAbort(ms, signal, sleepFn, abortMessage) {
|
|
|
645
630
|
export function classifyHandshakeError(err, { retry429 = true } = {}) {
|
|
646
631
|
if (!err) return null
|
|
647
632
|
const code = err.code || ''
|
|
648
|
-
const msg = String(err.message || '')
|
|
649
633
|
const status = Number(err.httpStatus || 0)
|
|
650
634
|
|
|
651
635
|
if (status === 401 || status === 426 || (status === 429 && !retry429)) return null
|
|
652
636
|
if (status > 0) {
|
|
653
|
-
|
|
637
|
+
// Typed transient handshake statuses only. A 403/404/4xx upgrade refusal
|
|
638
|
+
// is a deterministic decision: spending the retry budget on it cannot
|
|
639
|
+
// change the answer.
|
|
640
|
+
if (status === 429 || TRANSIENT_STATUSES.has(status) || (status >= 500 && status < 600)) {
|
|
641
|
+
return `http_${status}`
|
|
642
|
+
}
|
|
643
|
+
return null
|
|
654
644
|
}
|
|
655
645
|
|
|
656
646
|
if (code === 'ECONNRESET') return 'reset'
|
|
@@ -660,9 +650,6 @@ export function classifyHandshakeError(err, { retry429 = true } = {}) {
|
|
|
660
650
|
if (code === 'EWSACQUIRETIMEOUT') return 'acquire_timeout'
|
|
661
651
|
if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'EPIPE') return 'network'
|
|
662
652
|
|
|
663
|
-
if (/opening handshake has timed out/i.test(msg)) return 'timeout'
|
|
664
|
-
if (/socket hang up/i.test(msg)) return 'reset'
|
|
665
|
-
|
|
666
653
|
return null
|
|
667
654
|
}
|
|
668
655
|
|
|
@@ -677,9 +664,8 @@ export function classifyHandshakeError(err, { retry429 = true } = {}) {
|
|
|
677
664
|
* the provider's own responsibility via its native abort plumbing.
|
|
678
665
|
* - Uses classifyError() to decide retry. 'transient' → retry,
|
|
679
666
|
* 'auth' / 'permanent' / 'unknown' → throw immediately.
|
|
680
|
-
* -
|
|
681
|
-
*
|
|
682
|
-
* resolve to httpStatus before classification.
|
|
667
|
+
* - Classification is typed-only: an error with no status/errno/SDK type is
|
|
668
|
+
* 'unknown' and is surfaced immediately instead of being replayed.
|
|
683
669
|
*
|
|
684
670
|
* Returns whatever `fn()` resolves to. Throws the last error if every retry
|
|
685
671
|
* is exhausted, or the first error if it's classified non-transient.
|
|
@@ -736,16 +722,13 @@ export async function withRetry(fn, opts = {}) {
|
|
|
736
722
|
throw reason instanceof Error ? reason : new Error('withRetry: aborted')
|
|
737
723
|
}
|
|
738
724
|
lastErr = caught
|
|
739
|
-
populateHttpStatusFromMessage(caught)
|
|
740
725
|
const status = Number(caught?.httpStatus || caught?.status || caught?.response?.status || 0)
|
|
741
726
|
const kind = classifyError(caught)
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|| caught?.emittedThinking === true
|
|
748
|
-
if (outputWasExposed || caught?.unsafeToRetry === true) throw caught
|
|
727
|
+
// Hard replay boundary: a retry RE-ISSUES the request, so it is denied
|
|
728
|
+
// once visible output was relayed or a tool call was dispatched. Whether
|
|
729
|
+
// an eligible failure is actually retried remains the typed question
|
|
730
|
+
// resolved by classifyError()/status below.
|
|
731
|
+
if (readStreamOutcome(caught).replaySafe !== true) throw caught
|
|
749
732
|
// Claude Code treats x-should-retry:false as an explicit server veto
|
|
750
733
|
// (except an internal-only 5xx override that Mixdog does not have).
|
|
751
734
|
// Keep this ahead of status defaults, including the request-local 429 path.
|