mixdog 0.9.0 → 0.9.2

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.
Files changed (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -126,7 +126,7 @@ export function classifyError(err) {
126
126
  || code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA'
127
127
  || code === 'ECONNREFUSED' || code === 'ENETUNREACH' || code === 'EHOSTUNREACH'
128
128
  || code === 'EPIPE'
129
- || code === 'EPROVIDERTIMEOUT' || code === 'EGEMINITIMEOUT'
129
+ || code === 'EPROVIDERTIMEOUT' || code === 'EGEMINITIMEOUT' || code === 'ESTREAMSTALL'
130
130
  || code === 'EWSACQUIRETIMEOUT') {
131
131
  return 'transient'
132
132
  }
@@ -231,6 +231,270 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
231
231
  return Math.max(0, Math.round(base + offset))
232
232
  }
233
233
 
234
+ // ── Shared network-resilience interface ──────────────────────────────────────
235
+ // One home for the logic that used to be copied per provider: mid-stream
236
+ // classifier (WS + SSE), transport fallback predicate, stream-safety stamp
237
+ // latches, abort-aware sleep, handshake classifier, and the retry-budget table.
238
+ // Provider differences are passed as ARGUMENTS (policy objects), never branched
239
+ // on a hardcoded provider name. Behavior is preserved EXACTLY — each path below
240
+ // is the relocated original, selected by policy.mode.
241
+
242
+ // F) Retry-budget profiles as DATA. The numbers live ONLY here now.
243
+ // ws.transientCloseRetries (4) — ws_1006 / ws_1011 connection-loss buckets.
244
+ // ws.defaultRetries (2) — every other WS mid-stream bucket.
245
+ // sse.defaultRetries (3) — anthropic single-shot SSE mid-stream budget.
246
+ export const MIDSTREAM_RETRY_POLICY = {
247
+ ws: { transientCloseRetries: 4, defaultRetries: 2, backoff: [250, 1000, 2000, 4000] },
248
+ sse: { defaultRetries: 3, backoff: [250, 1000, 2000, 4000] },
249
+ }
250
+
251
+ // WS buckets that earn the larger transient-close retry budget.
252
+ const WS_TRANSIENT_CLOSE_CLASSIFIERS = new Set(['ws_1006', 'ws_1011'])
253
+
254
+ function _midstreamLimitFor(classifier, policy) {
255
+ if (policy.mode === 'ws') {
256
+ return WS_TRANSIENT_CLOSE_CLASSIFIERS.has(classifier)
257
+ ? policy.transientCloseRetries
258
+ : policy.defaultRetries
259
+ }
260
+ return policy.defaultRetries
261
+ }
262
+
263
+ // WS gates each classifier against its own budget (mirrors the old
264
+ // _allowMidstreamRetry). SSE applies a single top-of-function budget gate and
265
+ // then returns raw classifier strings, so perClassifierGate:false returns the
266
+ // classifier unconditionally here.
267
+ function _allowMidstream(classifier, attemptIndex, policy) {
268
+ if (policy.perClassifierGate === false) return classifier
269
+ return attemptIndex < _midstreamLimitFor(classifier, policy) ? classifier : null
270
+ }
271
+
272
+ // A) Unified mid-stream classifier. Returns a classifier string or null.
273
+ // `signals` is the provider's mid-stream state object (field names unchanged
274
+ // from each provider's midState). `policy.mode` selects the WS or SSE path so
275
+ // both providers reproduce their exact current branch order and gating.
276
+ export function classifyMidstreamError(err, signals, policy = {}) {
277
+ if (!signals) return null
278
+ const attemptIndex = signals.attemptIndex | 0
279
+ if (policy.mode === 'sse') return _classifyMidstreamSse(err, signals, attemptIndex, policy)
280
+ return _classifyMidstreamWs(err, signals, attemptIndex, policy)
281
+ }
282
+
283
+ // Verbatim relocation of openai-oauth-ws's _classifyMidstreamError. `signals`
284
+ // carries sawResponseCreated / sawCompleted / emittedText / emittedToolCall /
285
+ // wsCloseCode / firstByteTimeout / wsSendFailed /
286
+ // userAbort / watchdogAbort / responseFailedPayload exactly as before.
287
+ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
288
+ if (state.sawCompleted) return null
289
+ if (state.emittedToolCall) {
290
+ const _cc = Number(err?.wsCloseCode || state.wsCloseCode || 0)
291
+ if (!(_cc === 1000 && state.sawResponseCreated && !state.sawCompleted)) return null
292
+ }
293
+ if (state.emittedText || err?.liveTextEmitted) return null
294
+ if (state.firstByteTimeout || err?.firstByteTimeout) {
295
+ return _allowMidstream('first_byte_timeout', attemptIndex, policy)
296
+ }
297
+ if (err?.wsSendFailed || state.wsSendFailed) {
298
+ return _allowMidstream('ws_send_failed', attemptIndex, policy)
299
+ }
300
+ if (!state.sawResponseCreated) {
301
+ const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
302
+ if (closeCode !== 1011 && closeCode !== 1012) return null
303
+ }
304
+ if (state.userAbort) return null
305
+
306
+ if (!err) return null
307
+ const status = Number(err?.httpStatus || 0)
308
+ if (status === 401 || status === 403 || status === 429) return null
309
+ if (status >= 500 && status < 600) {
310
+ return _allowMidstream(`http_${status}`, attemptIndex, policy)
311
+ }
312
+
313
+ const name = err?.name || ''
314
+ if (name === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
315
+ if (name === 'StreamStalledAbortError' || name === 'StreamStalledError' || err?.code === 'ESTREAMSTALL' || err?.streamStalled === true) {
316
+ // A stall that fired AFTER a tool call was emitted is unsafe to replay
317
+ // (double side-effect); surface it as terminal so the turn is not retried.
318
+ if (err?.unsafeToRetry === true) return null
319
+ return _allowMidstream('stream_stalled', attemptIndex, policy)
320
+ }
321
+
322
+ if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
323
+ if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstream('stream_stalled', attemptIndex, policy)
324
+
325
+ const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
326
+ if (closeCode === 1006) return _allowMidstream('ws_1006', attemptIndex, policy)
327
+ if (closeCode === 1011) return _allowMidstream('ws_1011', attemptIndex, policy)
328
+ if (closeCode === 1012) return _allowMidstream('ws_1012', attemptIndex, policy)
329
+ if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null
330
+ if (closeCode === 4000) return _allowMidstream('ws_4000', attemptIndex, policy)
331
+ if (closeCode === 1000 && state.sawResponseCreated && !state.sawCompleted) return _allowMidstream('ws_1000', attemptIndex, policy)
332
+
333
+ const failed = err?.responseFailed || state.responseFailedPayload
334
+ if (failed) {
335
+ try {
336
+ const blob = JSON.stringify(failed).toLowerCase()
337
+ if (blob.includes('stream_disconnected')) return _allowMidstream('response_failed_disconnected', attemptIndex, policy)
338
+ if (blob.includes('network_error')) return _allowMidstream('response_failed_network', attemptIndex, policy)
339
+ if (blob.includes('auth context expired')) return _allowMidstream('response_failed_auth_expired', attemptIndex, policy)
340
+ } catch {}
341
+ }
342
+
343
+ return null
344
+ }
345
+
346
+ // Verbatim relocation of anthropic-oauth's _classifyMidstreamError. `signals`
347
+ // carries sawMessageStart / sawCompleted / emittedToolCall / userAbort /
348
+ // watchdogAbort exactly as before.
349
+ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
350
+ if (attemptIndex >= policy.defaultRetries) return null
351
+ if (state.sawCompleted) return null
352
+ if (!state.sawMessageStart) return null
353
+ if (state.userAbort) return null
354
+ if (state.emittedToolCall) return null
355
+
356
+ if (!err) return null
357
+ const status = Number(err?.httpStatus || 0)
358
+ if (status === 401 || status === 403 || status === 429) return null
359
+
360
+ const name = err?.name || ''
361
+ if (name === 'AgentStallAbortError') return 'agent_stall'
362
+ if (name === 'StreamStalledAbortError' || name === 'StreamStalledError' || err?.code === 'ESTREAMSTALL' || err?.streamStalled === true) {
363
+ // A stall AFTER a tool emit is unsafe to replay (double side-effect) →
364
+ // terminal (null), no mid-stream retry. Otherwise route to stream_stalled.
365
+ if (err?.unsafeToRetry === true) return null
366
+ return 'stream_stalled'
367
+ }
368
+ if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall'
369
+ if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled'
370
+
371
+ const code = err?.code || err?.cause?.code || ''
372
+ if (code === 'ECONNRESET') return 'reset'
373
+ if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
374
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns'
375
+
376
+ const msg = String(err?.message || '').toLowerCase()
377
+ if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout'
378
+ if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated'
379
+ if (msg.includes('fetch failed')) return 'fetch_failed'
380
+
381
+ return null
382
+ }
383
+
384
+ // B) Unified transport (WS→HTTP) fallback predicate. Identical deny-order +
385
+ // allow-list to the two former copies; `enabled` replaces the per-provider
386
+ // env-flag check (caller computes the flag and passes it).
387
+ const TRANSPORT_FALLBACK_CLASSIFIERS = new Set([
388
+ 'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
389
+ 'first_byte_timeout',
390
+ 'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
391
+ 'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
392
+ 'ws_send_failed',
393
+ ])
394
+ const TRANSPORT_FALLBACK_ERRNO = new Set([
395
+ 'EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN',
396
+ 'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE',
397
+ ])
398
+
399
+ export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
400
+ if (!enabled) return false
401
+ if (signal?.aborted) return false
402
+ if (err?.liveTextEmitted === true) return false
403
+ if (err?.emittedToolCall === true || err?.unsafeToRetry === true) return false
404
+ const status = Number(err?.httpStatus || err?.status || 0)
405
+ if (status === 401 || status === 403 || status === 404 || status === 429) return false
406
+ if (status >= 500 && status < 600) return true
407
+ const code = String(err?.code || '')
408
+ if (TRANSPORT_FALLBACK_ERRNO.has(code)) return true
409
+ const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '')
410
+ if (TRANSPORT_FALLBACK_CLASSIFIERS.has(classifier)) return true
411
+ if (/^http_5\d\d$/.test(classifier)) return true
412
+ if (err?.firstByteTimeout) return true
413
+ const msg = String(err?.message || '')
414
+ return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg)
415
+ }
416
+
417
+ // C) Stream-safety stamp latches. Mirrors openai-oauth-ws's _stampLiveText /
418
+ // _stampTool: once text/tool has been marked, every subsequent throw path
419
+ // re-applies the liveTextEmitted/emittedToolCall + unsafeToRetry markers so
420
+ // no upstream gate can reissue the turn and concatenate attempts.
421
+ export function createStreamSafetyStamps() {
422
+ let textLatched = false
423
+ let toolLatched = false
424
+ const stampText = (e) => {
425
+ if (textLatched && e) { try { e.liveTextEmitted = true; e.unsafeToRetry = true } catch {} }
426
+ return e
427
+ }
428
+ const stampTool = (e) => {
429
+ if (toolLatched && e) { try { e.emittedToolCall = true; e.unsafeToRetry = true } catch {} }
430
+ return e
431
+ }
432
+ return {
433
+ markText() { textLatched = true },
434
+ markTool() { toolLatched = true },
435
+ stampText,
436
+ stampTool,
437
+ stampAll: (e) => stampTool(stampText(e)),
438
+ }
439
+ }
440
+
441
+ const _defaultAbortSleep = (ms) => new Promise((r) => setTimeout(r, ms))
442
+
443
+ // D) Abort-aware sleep (single copy). Resolves after `ms`, or rejects with the
444
+ // signal's reason (or `abortMessage`) the moment the signal aborts. `sleepFn`
445
+ // is the injectable no-signal sleep (test seam); `abortMessage` preserves
446
+ // each caller's prior fallback text when the abort reason is not an Error.
447
+ export async function sleepWithAbort(ms, signal, sleepFn = _defaultAbortSleep, abortMessage = 'sleep aborted') {
448
+ if (!ms) return
449
+ if (!signal) {
450
+ await (sleepFn || _defaultAbortSleep)(ms)
451
+ return
452
+ }
453
+ await new Promise((resolve, reject) => {
454
+ const t = setTimeout(() => {
455
+ try { signal.removeEventListener('abort', onAbort) } catch {}
456
+ resolve()
457
+ }, ms)
458
+ const onAbort = () => {
459
+ clearTimeout(t)
460
+ const reason = signal.reason
461
+ reject(reason instanceof Error ? reason : new Error(abortMessage))
462
+ }
463
+ if (signal.aborted) { onAbort(); return }
464
+ signal.addEventListener('abort', onAbort, { once: true })
465
+ })
466
+ }
467
+
468
+ // E) Handshake classifier (moved here from openai-oauth-ws). Default-deny:
469
+ // anything not recognized as transient returns null. 401/403/404/429 are
470
+ // permanent. Returns 'timeout' | 'reset' | 'dns' | 'refused' | 'network' |
471
+ // 'acquire_timeout' | `http_5xx` | null.
472
+ export function classifyHandshakeError(err) {
473
+ if (!err) return null
474
+ const code = err.code || ''
475
+ const msg = String(err.message || '')
476
+ const status = Number(err.httpStatus || 0)
477
+
478
+ if (status === 401 || status === 403 || status === 404 || status === 429) {
479
+ return null
480
+ }
481
+ if (status >= 500 && status < 600) {
482
+ return `http_${status}`
483
+ }
484
+
485
+ if (code === 'ECONNRESET') return 'reset'
486
+ if (code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA') return 'dns'
487
+ if (code === 'ECONNREFUSED') return 'refused'
488
+ if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
489
+ if (code === 'EWSACQUIRETIMEOUT') return 'acquire_timeout'
490
+ if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'EPIPE') return 'network'
491
+
492
+ if (/opening handshake has timed out/i.test(msg)) return 'timeout'
493
+ if (/socket hang up/i.test(msg)) return 'reset'
494
+
495
+ return null
496
+ }
497
+
234
498
  /**
235
499
  * Run an async function with exponential-backoff retry on transient errors.
236
500
  *