mixdog 0.9.95 → 0.9.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
3
- import { withRetry } from './retry-classifier.mjs';
3
+ import { canFallbackNonStreaming, withRetry } from './retry-classifier.mjs';
4
4
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
5
5
  import { sendViaWebSocket } from './openai-oauth-ws.mjs';
6
6
  import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
@@ -69,6 +69,16 @@ function loadOpenAI() {
69
69
  return _OpenAI;
70
70
  }
71
71
 
72
+ // Grok OAuth creates its credential-bound inner provider only when send() has
73
+ // the final token and request headers. Let the process-wide provider warmup pay
74
+ // the OpenAI SDK + undici pool load before that first request reaches the hot
75
+ // path, without constructing a fake credential-bound client.
76
+ export function preloadOpenAICompatRuntime() {
77
+ const OpenAI = loadOpenAI();
78
+ getLlmDispatcher();
79
+ return OpenAI;
80
+ }
81
+
72
82
  function attachCompletedWarmup(err, warmup) {
73
83
  if (!err || !warmup?.usage) return err;
74
84
  try {
@@ -238,7 +248,8 @@ export class OpenAICompatProvider {
238
248
  // extraHeaders behave exactly as before.
239
249
  this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(config.extraHeaders || {}) };
240
250
  this.defaultModel = preset?.defaultModel || 'default';
241
- this.client = new (loadOpenAI())({
251
+ const OpenAI = preloadOpenAICompatRuntime();
252
+ this.client = new OpenAI({
242
253
  baseURL,
243
254
  apiKey,
244
255
  defaultHeaders: this.defaultHeaders,
@@ -253,6 +264,12 @@ export class OpenAICompatProvider {
253
264
  // a harmless no-op then.
254
265
  fetchOptions: { dispatcher: getLlmDispatcher() },
255
266
  });
267
+ // Provider registry initialization normally runs during daemon warmup.
268
+ // Start the origin handshake there; the per-send call below remains as
269
+ // the TTL-gated rewarm after long idle gaps.
270
+ if (this.config?.preconnect !== false) {
271
+ try { this._preconnectFn(this.baseURL); } catch { /* best-effort */ }
272
+ }
256
273
  }
257
274
  get _preconnectFn() {
258
275
  return typeof this.config?.preconnectFn === 'function'
@@ -420,44 +437,52 @@ export class OpenAICompatProvider {
420
437
  params.stream_options = { include_usage: true };
421
438
  let assembled;
422
439
  try {
423
- assembled = await withRetry(
424
- async ({ signal: attemptSignal }) => {
425
- try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
426
- const stream = await withRetry(
427
- ({ signal: openSignal }) => this.client.chat.completions.create(params, { signal: openSignal }),
428
- {
440
+ try {
441
+ assembled = await withRetry(
442
+ async ({ signal: attemptSignal }) => {
443
+ try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
444
+ const stream = await withRetry(
445
+ ({ signal: openSignal }) => this.client.chat.completions.create(params, { signal: openSignal }),
446
+ {
447
+ signal: attemptSignal,
448
+ // Single attempt: this inner wrapper exists only to
449
+ // apply the first-byte per-attempt timeout. Retry is
450
+ // owned by the outer withRetry — nesting retry loops
451
+ // here multiplied tail latency (5x5).
452
+ maxAttempts: 1,
453
+ perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
454
+ perAttemptLabel: `${this.name} first byte`,
455
+ },
456
+ );
457
+ try { opts.onStageChange?.('streaming'); } catch { /* heartbeat best-effort */ }
458
+ return consumeCompatChatCompletionStream(stream, {
429
459
  signal: attemptSignal,
430
- // Single attempt: this inner wrapper exists only to
431
- // apply the first-byte per-attempt timeout. Retry is
432
- // owned by the outer withRetry — nesting retry loops
433
- // here multiplied tail latency (5x5).
434
- maxAttempts: 1,
435
- perAttemptTimeoutMs: PROVIDER_FIRST_BYTE_TIMEOUT_MS,
436
- perAttemptLabel: `${this.name} first byte`,
460
+ label: this.name,
461
+ onStreamDelta: opts.onStreamDelta,
462
+ onToolCall: opts.onToolCall,
463
+ onTextDelta: opts.onTextDelta,
464
+ parseToolCalls,
465
+ // Known tool names for the leaked-tool-call guard:
466
+ // recovered leaked calls only synthesize when they name
467
+ // a tool actually offered to this request.
468
+ knownToolNames: knownToolNamesFromOpenAITools(params.tools),
469
+ });
470
+ },
471
+ {
472
+ signal: totalSignal.signal,
473
+ onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
474
+ const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
475
+ process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
437
476
  },
438
- );
439
- try { opts.onStageChange?.('streaming'); } catch { /* heartbeat best-effort */ }
440
- return consumeCompatChatCompletionStream(stream, {
441
- signal: attemptSignal,
442
- label: this.name,
443
- onStreamDelta: opts.onStreamDelta,
444
- onToolCall: opts.onToolCall,
445
- onTextDelta: opts.onTextDelta,
446
- parseToolCalls,
447
- // Known tool names for the leaked-tool-call guard:
448
- // recovered leaked calls only synthesize when they name
449
- // a tool actually offered to this request.
450
- knownToolNames: knownToolNamesFromOpenAITools(params.tools),
451
- });
452
- },
453
- {
454
- signal: totalSignal.signal,
455
- onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
456
- const delayLabel = Number.isFinite(Number(delayMs)) ? `, delay ${delayMs}ms${delayReason ? ` (${delayReason})` : ''}` : '';
457
- process.stderr.write(`[${this.name}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`);
458
477
  },
459
- },
460
- );
478
+ );
479
+ } catch (streamErr) {
480
+ const recovered = await this._recoverCompatNonStreaming({
481
+ streamErr, params, opts, signal: totalSignal.signal, useModel,
482
+ });
483
+ if (!recovered) throw streamErr;
484
+ assembled = recovered;
485
+ }
461
486
  } finally {
462
487
  totalSignal.cleanup();
463
488
  }
@@ -567,6 +592,83 @@ export class OpenAICompatProvider {
567
592
  })() : undefined,
568
593
  };
569
594
  }
595
+ /**
596
+ * cc parity (claude.ts:2504): when a stream dies, re-issue the SAME request
597
+ * with `stream:false` rather than failing the turn. Narrower than cc on
598
+ * purpose — canFallbackNonStreaming() clears only a stream that exposed
599
+ * NOTHING, so an eagerly dispatched tool can never run twice and rendered
600
+ * text is never duplicated (that case belongs to the loop's retraction
601
+ * replay). Returns the same assembled shape the stream consumer produces,
602
+ * or null when the failure is ineligible or the fallback itself fails.
603
+ */
604
+ async _recoverCompatNonStreaming({ streamErr, params, opts, signal, useModel }) {
605
+ if (!canFallbackNonStreaming(streamErr, { signal })) return null;
606
+ const nonStreamParams = { ...params, stream: false };
607
+ delete nonStreamParams.stream_options;
608
+ let response;
609
+ try {
610
+ try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
611
+ response = await withRetry(
612
+ ({ signal: attemptSignal }) => this.client.chat.completions.create(
613
+ nonStreamParams,
614
+ { signal: attemptSignal },
615
+ ),
616
+ {
617
+ signal,
618
+ maxAttempts: 1,
619
+ // A non-streaming call returns only when generation is done,
620
+ // so the first-byte bound would false-abort it.
621
+ perAttemptTimeoutMs: PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
622
+ perAttemptLabel: `${this.name} non-streaming fallback`,
623
+ },
624
+ );
625
+ } catch {
626
+ return null;
627
+ }
628
+ const choice = response?.choices?.[0] || null;
629
+ if (!choice) return null;
630
+ const message = choice.message || {};
631
+ let toolCalls;
632
+ try {
633
+ toolCalls = parseToolCalls(choice, this.name);
634
+ } catch {
635
+ return null;
636
+ }
637
+ try {
638
+ process.stderr.write(
639
+ `[${this.name}] stream failed (${streamErr?.code || streamErr?.message || 'unknown'}); `
640
+ + `recovered via non-streaming request\n`,
641
+ );
642
+ } catch { /* best-effort */ }
643
+ try {
644
+ appendAgentTrace({
645
+ sessionId: opts?.sessionId || opts?.session?.id || null,
646
+ iteration: Number.isFinite(Number(opts?.iteration)) ? Number(opts.iteration) : null,
647
+ kind: 'transport_fallback',
648
+ provider: this.name,
649
+ model: useModel,
650
+ transport: 'non-streaming',
651
+ payload: {
652
+ from: 'stream',
653
+ to: 'non-streaming',
654
+ reason: streamErr?.retryClassifier || streamErr?.code || streamErr?.message || 'stream_failed',
655
+ error_code: streamErr?.code || null,
656
+ error_http_status: Number(streamErr?.httpStatus || streamErr?.status || 0) || null,
657
+ error_classifier: streamErr?.retryClassifier || streamErr?.midstreamClassifier || null,
658
+ },
659
+ });
660
+ } catch { /* best-effort */ }
661
+ return {
662
+ response,
663
+ model: response.model || useModel,
664
+ content: typeof message.content === 'string' ? message.content : '',
665
+ toolCalls,
666
+ stopReason: choice.finish_reason || null,
667
+ reasoningContent: typeof message.reasoning_content === 'string' ? message.reasoning_content : null,
668
+ rawUsage: response.usage || null,
669
+ };
670
+ }
671
+
570
672
  async _doSendXaiResponses(messages, useModel, tools, opts) {
571
673
  const signal = opts.signal || null;
572
674
  if (signal?.aborted) {
@@ -589,8 +691,11 @@ export class OpenAICompatProvider {
589
691
  // existing request bodies.
590
692
  store: false,
591
693
  include: ['reasoning.encrypted_content'],
592
- prompt_cache_key: cacheRouting.key,
593
694
  };
695
+ // A null routing key means "omit the field" (cache scope 'none'):
696
+ // xAI's own client sends no prompt_cache_key and relies on the
697
+ // service's automatic prefix caching.
698
+ if (cacheRouting.key) params.prompt_cache_key = cacheRouting.key;
594
699
  if (previousResponseId) params.previous_response_id = previousResponseId;
595
700
  const nativeTools = nativeResponsesTools(opts);
596
701
  if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [], { provider: 'xai' })];
@@ -797,8 +902,8 @@ export class OpenAICompatProvider {
797
902
  input,
798
903
  store: false,
799
904
  include: ['reasoning.encrypted_content'],
800
- prompt_cache_key: cacheRouting.key,
801
905
  };
906
+ if (cacheRouting.key) params.prompt_cache_key = cacheRouting.key;
802
907
  const instructions = xaiSystemInstructions(messages);
803
908
  if (previousResponseId) params.previous_response_id = previousResponseId;
804
909
  // xAI rejects instructions together with previous_response_id; the
@@ -38,6 +38,7 @@ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './ant
38
38
  import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
39
39
  import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL, _displayCodexModel } from './openai-oauth.mjs';
40
40
  import { createActiveToolItemTracker } from './tool-stream-state.mjs';
41
+ import { parseProviderJsonBatch } from './stream-json-pool.mjs';
41
42
  export { envPositiveInt as _envPositiveInt } from './lib/env-utils.mjs';
42
43
 
43
44
  // Public OpenAI Responses API endpoint for the api-key `openai` provider.
@@ -114,6 +115,18 @@ function _parseSseFrame(frame) {
114
115
  try { return JSON.parse(raw); } catch { return null; }
115
116
  }
116
117
 
118
+ function _sseJsonPayload(frame) {
119
+ const lines = String(frame || '').split('\n');
120
+ const data = [];
121
+ for (const line of lines) {
122
+ if (!line || line.startsWith(':')) continue;
123
+ if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
124
+ }
125
+ if (!data.length) return null;
126
+ const raw = data.join('\n').trim();
127
+ return !raw || raw === '[DONE]' ? null : raw;
128
+ }
129
+
117
130
  function _incompleteReasonFromEvent(event) {
118
131
  const reasonObj = event?.response?.incomplete_details
119
132
  || event?.incomplete_details
@@ -1003,10 +1016,12 @@ export async function sendViaHttpSse({
1003
1016
  buffer += decoder.decode(value, { stream: true });
1004
1017
  const parsed = _sseEventsFromBuffer(buffer);
1005
1018
  buffer = parsed.rest;
1006
- for (const frame of parsed.frames) {
1007
- const event = _parseSseFrame(frame);
1008
- if (event) handleEvent(event);
1009
- }
1019
+ const payloads = parsed.frames.map(_sseJsonPayload).filter((payload) => payload !== null);
1020
+ // These bytes have already been delivered by reader.read(). Finish
1021
+ // this bounded chunk before observing abort on the next read so
1022
+ // visible text is never lost at the cancellation boundary.
1023
+ const events = await parseProviderJsonBatch(payloads);
1024
+ for (const event of events) handleEvent(event);
1010
1025
  }
1011
1026
  // The read() above can unblock via reader.cancel() as {done:true} on an
1012
1027
  // external/total-timeout abort. Surface that as the abort/timeout error
@@ -1014,10 +1029,9 @@ export async function sendViaHttpSse({
1014
1029
  if (_streamAbortReason) throw _streamAbortReason;
1015
1030
  buffer += decoder.decode();
1016
1031
  const parsed = _sseEventsFromBuffer(buffer + '\n\n');
1017
- for (const frame of parsed.frames) {
1018
- const event = _parseSseFrame(frame);
1019
- if (event) handleEvent(event);
1020
- }
1032
+ const payloads = parsed.frames.map(_sseJsonPayload).filter((payload) => payload !== null);
1033
+ const events = await parseProviderJsonBatch(payloads);
1034
+ for (const event of events) handleEvent(event);
1021
1035
  // Flush any partial-sentinel tail held back mid-stream so legitimate
1022
1036
  // trailing text is never lost (streamed-text path).
1023
1037
  flushLeak();
@@ -467,6 +467,20 @@ export class OpenAIOAuthProvider {
467
467
  }
468
468
  throw new Error(`OpenAI OAuth token refresh failed (${msg}). Re-authenticate via provider login.`);
469
469
  }
470
+ }, {
471
+ // OAuth refresh includes network exchange plus owner-only atomic
472
+ // persistence. Match the Anthropic/xAI lease: the generic 2s file
473
+ // lock timeout is too short under Windows ACL and process contention.
474
+ timeoutMs: 120_000,
475
+ staleMs: 120_000,
476
+ secret: true,
477
+ }, {
478
+ // OAuth refresh includes network exchange plus owner-only atomic
479
+ // persistence. Match the Anthropic/xAI lease: the generic 2s file
480
+ // lock timeout is too short under Windows ACL and process contention.
481
+ timeoutMs: 120_000,
482
+ staleMs: 120_000,
483
+ secret: true,
470
484
  }).finally(() => { _oauthRefreshInFlight = null; });
471
485
 
472
486
  this.tokens = await _oauthRefreshInFlight;
@@ -651,7 +665,7 @@ export class OpenAIOAuthProvider {
651
665
  // properties (instructions/tools/etc.) but never send the live
652
666
  // transcript/user input. The completed generate:false response is
653
667
  // retained by the WS transport and anchors the first real request.
654
- warmupBody: _envFlag('MIXDOG_OPENAI_OAUTH_WS_WARMUP', true)
668
+ warmupBody: _envFlag('MIXDOG_OPENAI_OAUTH_WS_WARMUP', false)
655
669
  ? { ...body, input: [], generate: false }
656
670
  : null,
657
671
  _carriedWarmup: carriedWarmup,
@@ -113,7 +113,7 @@ export function resolveResponsesTransportPolicy(env = process.env, capabilities
113
113
  mode,
114
114
  requestedMode,
115
115
  transport,
116
- // Codex refs behavior: default/auto is WS-first but not WS-only. If the
116
+ // Reference behavior: default/auto is WS-first but not WS-only. If the
117
117
  // websocket path stalls/fails before emitting live output, callers may
118
118
  // replay the request over HTTP/SSE. Explicit ws-* modes remain pinned
119
119
  // for transport experiments; explicit http-sse bypasses WS entirely.
@@ -323,10 +323,9 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
323
323
  'chatgpt-account-id': auth.account_id || '',
324
324
  'originator': codexOriginator(),
325
325
  'OpenAI-Beta': 'responses_websockets=2026-02-06',
326
- // codex-rs merges provider http_headers ("version") plus
327
- // default_headers (User-Agent) into the WS handshake
328
- // (client.rs:970-975, model-provider-info/src/lib.rs:339-343,
329
- // login default_client.rs:289-305). The backend fingerprints
326
+ // The reference client merges provider http_headers ("version")
327
+ // plus default headers (User-Agent) into the WS handshake.
328
+ // The backend fingerprints
330
329
  // clients on these; missing them can route us onto a different
331
330
  // (colder) cache-node class than codex.
332
331
  'User-Agent': codexUserAgent(),
@@ -535,12 +535,9 @@ export async function _streamResponse({
535
535
  // only) trips a short, named terminal StreamStalledError instead of coasting
536
536
  // to the 300s inter-chunk cap / 30-min agent watchdog.
537
537
  let semanticIdleTimer = null;
538
- // WS gets its own shorter semantic-idle window. The shared SSE semantic
539
- // timeout is intentionally near the 300s stall warning to protect Anthropic
540
- // effort-mode "silent thinking"; using that value here lets a wedged WS
541
- // request race the TUI 300s watchdog. Keep websocket stalls provider-owned:
542
- // fail/retry/fallback here first, then let the TUI watchdog remain a final
543
- // safety net only.
538
+ // WS owns its semantic-idle window. The TUI backstop is deliberately much
539
+ // longer, so this timeout can fail/retry/fallback at the provider boundary
540
+ // without racing an outer turn abort (Claude Code/Codex parity).
544
541
  const semanticIdleMs = PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS;
545
542
  const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
546
543
  // First-meaningful-frame watchdog timer + one-shot latch. Armed alongside
@@ -35,7 +35,7 @@ let _initChain = Promise.resolve();
35
35
  let _inFlightPromise = null;
36
36
  let _inFlightSig = null;
37
37
  let _lastAppliedSig = null;
38
- // Provider instances are process-global inside the backend daemon. Catalog
38
+ // Provider instances are process-global inside the daemon. Catalog
39
39
  // readers use this revision to share one raw model snapshot while still
40
40
  // rebuilding their cheap route/config projection after auth/config changes.
41
41
  let _providerCatalogRevision = 0;
@@ -367,7 +367,7 @@ function warmupCatalogs() {
367
367
  }
368
368
  }
369
369
 
370
- // Force-refresh each provider's /models catalog ONCE per backend-daemon
370
+ // Force-refresh each provider's /models catalog once per daemon.
371
371
  // lifetime. Every session runtime joins this process-global promise and then
372
372
  // reads the same provider-instance caches. Unlike
373
373
  // warmupCatalogs (which calls listModels() and so respects the 24h provider
@@ -104,6 +104,15 @@ export function classifyError(err) {
104
104
  if (err.truncatedStream === true || err.code === 'TRUNCATED_STREAM') return 'transient'
105
105
 
106
106
  if (TRANSIENT_STATUSES.has(status) || (status >= 500 && status < 600)) return 'transient'
107
+ // A stream that closed WITHOUT its terminal frame is a transport symptom,
108
+ // not a model verdict: the socket carries no HTTP status and no Node errno,
109
+ // so without this it classified as 'unknown' and no loop-level replay was
110
+ // ever attempted (observed live: pooled WS retired by the server between
111
+ // turns → close 1000 before response.created → the whole turn failed).
112
+ // Reference behavior retries the same disconnect (codex `CodexErr::Stream`
113
+ // is_retryable, cc falls back/retries the stream); the exposure deny above
114
+ // still fails closed for anything already relayed or dispatched.
115
+ if (isNonTerminalStreamClose(err)) return 'transient'
107
116
 
108
117
  // Socket-level codes (Node errno) — DNS / reset / refused / timeout are all
109
118
  // transient: we can retry the same request and may succeed.
@@ -124,6 +133,50 @@ const TRANSIENT_ERROR_CODES = new Set([
124
133
  'EPROVIDERTIMEOUT', 'EGEMINITIMEOUT', 'ESTREAMSTALL', 'EWSACQUIRETIMEOUT',
125
134
  ])
126
135
 
136
+ // WebSocket close codes that can end a Responses stream BEFORE its terminal
137
+ // frame. 1000/1001/1005 are "nominal" closes the server sends when it retires
138
+ // a pooled socket; 1006/1011/1012 are abnormal/overload closes. None of them
139
+ // is a completed turn, and all are safe to re-issue when nothing was exposed.
140
+ // 4000 is our own local stall close and keeps the stall classification path.
141
+ const NON_TERMINAL_STREAM_CLOSE_CODES = new Set([1000, 1001, 1005, 1006, 1011, 1012])
142
+
143
+ /**
144
+ * True when the error describes a stream socket that closed without the
145
+ * provider's terminal frame (response.completed / message_stop). Replay
146
+ * PERMISSION is still owned by the stream-outcome contract; this predicate
147
+ * only answers "was this a transport-level disconnect".
148
+ */
149
+ export function isNonTerminalStreamClose(err) {
150
+ if (!err || typeof err !== 'object') return false
151
+ const code = Number(err.wsCloseCode ?? err.streamCloseCode ?? 0) || 0
152
+ if (!NON_TERMINAL_STREAM_CLOSE_CODES.has(code)) return false
153
+ return readStreamOutcome(err).terminalObserved !== true
154
+ }
155
+
156
+ /**
157
+ * Should this failed stream be re-issued as a NON-STREAMING request?
158
+ *
159
+ * cc's last safety net: when a stream dies it repeats the same request with
160
+ * `stream:false` instead of failing the turn (claude.ts, gated off only when
161
+ * streaming tool execution could double-run a tool). MixDog dispatches tools
162
+ * eagerly, so this stays deliberately narrower than cc: only a stream that
163
+ * exposed NOTHING qualifies. An exposed stream is already covered by the
164
+ * loop-level retraction replay (send-with-recovery), which asks the owner to
165
+ * withdraw the rendered characters first.
166
+ */
167
+ export function canFallbackNonStreaming(err, { signal } = {}) {
168
+ if (!err || signal?.aborted) return false
169
+ const outcome = readStreamOutcome(err)
170
+ // A completed turn needs no fallback; a user cancel must never be re-issued.
171
+ if (outcome.terminalObserved || outcome.userAbort) return false
172
+ // Exposure/dispatch fails closed — re-running would duplicate output or a
173
+ // side effect.
174
+ if (outcome.replaySafe !== true) return false
175
+ return classifyError(err) === 'transient'
176
+ || outcome.stallObserved === true
177
+ || outcome.truncatedStream === true
178
+ }
179
+
127
180
  function boundedCauseChain(err) {
128
181
  const chain = []
129
182
  const seen = new Set()
@@ -435,11 +488,13 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
435
488
  }
436
489
  if (!state.sawResponseCreated) {
437
490
  const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
438
- // An abnormal close before response.created has not produced any response
439
- // bytes to the caller. It is therefore safe to reconnect and replay under
440
- // the normal ws_1006 bounded retry policy (text/tool emission was denied
441
- // above before reaching this gate).
442
- if (closeCode !== 1006 && closeCode !== 1011 && closeCode !== 1012) return null
491
+ // A close before response.created has not produced any response bytes to
492
+ // the caller, so it is safe to reconnect and replay under the bounded
493
+ // retry policy (text/tool emission was denied above before this gate).
494
+ // NOMINAL closes count too: a pooled socket the server retires between
495
+ // turns closes with 1000/1001/1005 and must reconnect fresh — treating
496
+ // that as terminal killed the turn instead of reissuing it.
497
+ if (!NON_TERMINAL_STREAM_CLOSE_CODES.has(closeCode)) return null
443
498
  }
444
499
  if (state.userAbort) return null
445
500
 
@@ -465,7 +520,11 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
465
520
  if (closeCode === 1012) return _allowMidstream('ws_1012', attemptIndex, policy)
466
521
  if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null
467
522
  if (closeCode === 4000) return _allowMidstream('ws_4000', attemptIndex, policy)
468
- if (closeCode === 1000 && state.sawResponseCreated && !state.sawCompleted) return _allowMidstream('ws_1000', attemptIndex, policy)
523
+ // Nominal close without the terminal frame — before OR after
524
+ // response.created. Only a completed turn is terminal here.
525
+ if ((closeCode === 1000 || closeCode === 1001 || closeCode === 1005) && !state.sawCompleted) {
526
+ return _allowMidstream(`ws_${closeCode}`, attemptIndex, policy)
527
+ }
469
528
 
470
529
  const failed = err?.responseFailed || state.responseFailedPayload
471
530
  if (failed) {
@@ -536,7 +595,8 @@ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
536
595
  const TRANSPORT_FALLBACK_CLASSIFIERS = new Set([
537
596
  'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
538
597
  'first_byte_timeout',
539
- 'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
598
+ 'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_1001', 'ws_1005', 'ws_4000',
599
+ 'agent_stall', 'stream_stalled',
540
600
  'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
541
601
  'ws_send_failed',
542
602
  ])