mixdog 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * OpenAI Direct API — WebSocket transport via Responses API.
3
3
  *
4
- * Uses the same `sendViaWebSocket` plumbing as openai-oauth (Codex), with two
4
+ * Uses the same `sendViaWebSocket` plumbing as openai-oauth, with two
5
5
  * differences encoded in the `auth.type === 'openai-direct'` branch inside
6
6
  * openai-oauth-ws.mjs:
7
7
  * 1. Authorization header: Bearer <OPENAI_API_KEY> (no account_id, no
@@ -18,7 +18,7 @@ import { enrichModels } from './model-catalog.mjs';
18
18
  import {
19
19
  resolveProviderCacheKey,
20
20
  resolveProviderPromptCacheLane,
21
- } from '../smart-bridge/cache-strategy.mjs';
21
+ } from '../agent-runtime/cache-strategy.mjs';
22
22
 
23
23
  const OPENAI_DIRECT_PRIORITY_MODEL_PATTERNS = Object.freeze([
24
24
  /^gpt-5\.5(?:-\d{4}|$)/,
@@ -67,13 +67,13 @@ export class OpenAIDirectProvider {
67
67
  promptCacheLane,
68
68
  });
69
69
  // Public OpenAI API priority support is documented separately from the
70
- // Codex OAuth catalog. Keep this provider's service-tier decision local
71
- // so gpt-5.4-mini can opt into Priority even when the Codex catalog does
70
+ // openai-oauth catalog. Keep this provider's service-tier decision local
71
+ // so gpt-5.4-mini can opt into Priority even when the OAuth catalog does
72
72
  // not advertise a Fast tier for its OAuth endpoint.
73
73
  applyOpenAIDirectFastTier(body, useModel, opts);
74
74
  // Public Responses API supports prompt_cache_retention='24h' at no
75
75
  // extra cost (same cached_input_tokens billing as the default 5–10
76
- // min in-memory cache). Codex/oauth rejects the parameter, so it's
76
+ // min in-memory cache). openai-oauth rejects the parameter, so it's
77
77
  // injected only on the direct path. See openai-oauth.mjs:290-294
78
78
  // for the rationale.
79
79
  body.prompt_cache_retention = '24h';
@@ -85,7 +85,7 @@ export class OpenAIDirectProvider {
85
85
  // cacheKey (prompt_cache_key) only groups the server-side prefix-cache
86
86
  // shard — safe to share across sessions, unlike the sessionId poolKey
87
87
  // above. buildRequestBody derives it from the base namespace plus a
88
- // model/system/tools hash, mirroring the Codex OAuth path while keeping
88
+ // model/system/tools hash, mirroring the openai-oauth path while keeping
89
89
  // public OpenAI's 24h retention below.
90
90
  const cacheKey = body.prompt_cache_key || resolveProviderCacheKey(opts, 'openai');
91
91
  const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
@@ -1,21 +1,30 @@
1
- import { OpenAICompatProvider, OPENAI_COMPAT_PRESETS } from './openai-compat.mjs';
2
- import { AnthropicProvider } from './anthropic.mjs';
3
- import { OpenCodeGoProvider } from './opencode-go.mjs';
4
- import { GeminiProvider } from './gemini.mjs';
5
- import { OpenAIOAuthProvider, hasOpenAIOAuthCredentials } from './openai-oauth.mjs';
6
- import { AnthropicOAuthProvider, hasAnthropicOAuthCredentials } from './anthropic-oauth.mjs';
7
- import { GrokOAuthProvider, hasGrokOAuthCredentials } from './grok-oauth.mjs';
8
- import { OpenAIDirectProvider } from './openai-ws.mjs';
1
+ import { OPENAI_COMPAT_PRESETS } from './openai-compat-presets.mjs';
2
+ import {
3
+ hasAnthropicOAuthCredentials,
4
+ hasOpenAIOAuthCredentials,
5
+ hasGrokOAuthCredentials,
6
+ } from './oauth-credential-probes.mjs';
9
7
  import { refreshCatalog as refreshMetadataCatalog, warmModelMetadataCatalogs } from './model-catalog.mjs';
10
- // OpenAI-compat provider names are self-declared by openai-compat.mjs via
8
+ // OpenAI-compat provider names are self-declared by openai-compat-presets.mjs via
11
9
  // OPENAI_COMPAT_PRESETS. No parallel list maintained here.
12
10
  const providers = new Map();
11
+ const providerCtors = new Map();
12
+ const providerModulePromises = new Map();
13
13
  // Parallel map: provider name -> signature of the config it was built from.
14
14
  // Lets initProviders() skip reconstructing a provider whose config is byte-for-
15
15
  // byte identical to the live one, so lazy-init misses that re-run initProviders
16
16
  // don't churn (tear down + rebuild) every live provider instance on every call.
17
17
  const signatures = new Map();
18
18
 
19
+ // Module-level init serialization. agent-tool.mjs's ensureProvider() already
20
+ // serializes inits per provider on a chain promise, but its gateOnPrior() lets
21
+ // a queued init PROCEED once a 120s gate expires even if the prior init has
22
+ // not settled — so a slow-but-still-running init and a newer one can reach
23
+ // initProviders() concurrently. This chain makes the clear()+rebuild section
24
+ // strictly sequential at the registry level, independent of any caller gating,
25
+ // so two different config signatures can never interleave their rebuilds.
26
+ let _initChain = Promise.resolve();
27
+
19
28
  // Deterministic structural signature of a provider config. Recursively sorts
20
29
  // object keys so signature equality reflects config-value equality regardless
21
30
  // of key insertion order. Invariant: same config in -> same string out.
@@ -37,7 +46,51 @@ function configSignature(cfg) {
37
46
  return null;
38
47
  }
39
48
  }
49
+
50
+ async function loadProviderExport(cacheKey, spec, exportName) {
51
+ if (!providerModulePromises.has(cacheKey)) {
52
+ providerModulePromises.set(cacheKey, import(spec));
53
+ }
54
+ const mod = await providerModulePromises.get(cacheKey);
55
+ const value = mod?.[exportName];
56
+ if (typeof value !== 'function') throw new Error(`provider export missing: ${exportName}`);
57
+ providerCtors.set(cacheKey, value);
58
+ return value;
59
+ }
60
+
61
+ async function loadProviderCtor(name) {
62
+ if (name === 'anthropic') return loadProviderExport('anthropic', './anthropic.mjs', 'AnthropicProvider');
63
+ if (name === 'gemini') return loadProviderExport('gemini', './gemini.mjs', 'GeminiProvider');
64
+ if (name === 'openai-oauth') return loadProviderExport('openai-oauth', './openai-oauth.mjs', 'OpenAIOAuthProvider');
65
+ if (name === 'anthropic-oauth') return loadProviderExport('anthropic-oauth', './anthropic-oauth.mjs', 'AnthropicOAuthProvider');
66
+ if (name === 'grok-oauth') return loadProviderExport('grok-oauth', './grok-oauth.mjs', 'GrokOAuthProvider');
67
+ if (name === 'openai') return loadProviderExport('openai', './openai-ws.mjs', 'OpenAIDirectProvider');
68
+ if (name === 'opencode-go') return loadProviderExport('opencode-go', './opencode-go.mjs', 'OpenCodeGoProvider');
69
+ if (Object.prototype.hasOwnProperty.call(OPENAI_COMPAT_PRESETS, name)) {
70
+ return loadProviderExport('openai-compat', './openai-compat.mjs', 'OpenAICompatProvider');
71
+ }
72
+ throw new Error(`unknown enabled provider: ${name}`);
73
+ }
74
+
75
+ function instantiateProvider(name, Ctor, cfg) {
76
+ if (Object.prototype.hasOwnProperty.call(OPENAI_COMPAT_PRESETS, name) && name !== 'opencode-go') {
77
+ return new Ctor(name, cfg);
78
+ }
79
+ return new Ctor(cfg);
80
+ }
81
+
40
82
  export async function initProviders(config) {
83
+ // Serialize ALL inits through a single chain so two different config
84
+ // signatures can never run their clear()+rebuild concurrently, regardless
85
+ // of caller-side gating (agent-tool gateOnPrior may release a queued init
86
+ // before the prior one settled). Errors do not poison the chain.
87
+ const run = () => _initProvidersUnsynchronized(config);
88
+ const next = _initChain.then(run, run);
89
+ _initChain = next.then(() => {}, () => {});
90
+ return next;
91
+ }
92
+
93
+ async function _initProvidersUnsynchronized(config) {
41
94
  // Invariant: never wipe the live registry based on an empty / all-disabled
42
95
  // config. Without this guard, a stale `loadAgentConfig()` (e.g. mid-reload
43
96
  // or a transient FS hiccup) would land here as `{}` or `{...,enabled:false}`,
@@ -51,54 +104,30 @@ export async function initProviders(config) {
51
104
  }
52
105
  const next = new Map();
53
106
  const nextSignatures = new Map();
54
- for (const [name, cfg] of entries) {
55
- if (!cfg.enabled)
56
- continue;
107
+ const enabledResults = await Promise.all(entries.map(async ([name, cfg]) => {
108
+ if (!cfg.enabled) return null;
57
109
  // Idempotent reuse: an enabled provider whose config signature is
58
110
  // unchanged from the live registry is carried forward as-is. Only
59
111
  // added or changed providers are (re)constructed below.
60
112
  const sig = configSignature(cfg);
61
113
  if (sig !== null && providers.has(name) && signatures.get(name) === sig) {
62
- next.set(name, providers.get(name));
63
- nextSignatures.set(name, sig);
64
- continue;
114
+ return { name, inst: providers.get(name), sig };
65
115
  }
66
116
  try {
67
- let inst;
68
- if (name === 'anthropic') {
69
- inst = new AnthropicProvider(cfg);
70
- }
71
- else if (name === 'gemini') {
72
- inst = new GeminiProvider(cfg);
73
- }
74
- else if (name === 'openai-oauth') {
75
- inst = new OpenAIOAuthProvider(cfg);
76
- }
77
- else if (name === 'anthropic-oauth') {
78
- inst = new AnthropicOAuthProvider(cfg);
79
- }
80
- else if (name === 'grok-oauth') {
81
- inst = new GrokOAuthProvider(cfg);
82
- }
83
- else if (name === 'openai') {
84
- inst = new OpenAIDirectProvider(cfg);
85
- }
86
- else if (name === 'opencode-go') {
87
- inst = new OpenCodeGoProvider(cfg);
88
- }
89
- else if (Object.prototype.hasOwnProperty.call(OPENAI_COMPAT_PRESETS, name)) {
90
- inst = new OpenAICompatProvider(name, cfg);
91
- }
92
- else {
93
- throw new Error(`unknown enabled provider: ${name}`);
94
- }
95
- next.set(name, inst);
96
- if (sig !== null) nextSignatures.set(name, sig);
117
+ const Ctor = await loadProviderCtor(name);
118
+ const inst = instantiateProvider(name, Ctor, cfg);
119
+ return { name, inst, sig };
97
120
  }
98
121
  catch (err) {
99
122
  const msg = err instanceof Error ? err.message : String(err);
100
- throw new Error(`[provider] Failed to init "${name}": ${msg}`);
123
+ return { name, error: new Error(`[provider] Failed to init "${name}": ${msg}`) };
101
124
  }
125
+ }));
126
+ for (const result of enabledResults) {
127
+ if (!result) continue;
128
+ if (result.error) throw result.error;
129
+ next.set(result.name, result.inst);
130
+ if (result.sig !== null) nextSignatures.set(result.name, result.sig);
102
131
  }
103
132
  // Second guard: every entry was disabled. Same reasoning — keep the
104
133
  // existing registry rather than going dark.
@@ -136,19 +165,27 @@ export function getProvider(name) {
136
165
  // failure) — initProviders then skipped the entry entirely so there is
137
166
  // nothing for the preservation guard to carry forward. Re-probe the
138
167
  // credential each miss: if the credential is now valid, register the
139
- // instance on the spot so subsequent calls hit the cached entry.
168
+ // instance on the spot when that provider module has already been loaded,
169
+ // so subsequent calls hit the cached entry without making registry import
170
+ // pay every provider runtime at boot.
140
171
  if (name === 'anthropic-oauth' && hasAnthropicOAuthCredentials()) {
141
- const inst = new AnthropicOAuthProvider({});
172
+ const Ctor = providerCtors.get('anthropic-oauth');
173
+ if (!Ctor) return undefined;
174
+ const inst = new Ctor({});
142
175
  providers.set(name, inst);
143
176
  return inst;
144
177
  }
145
178
  if (name === 'openai-oauth' && hasOpenAIOAuthCredentials()) {
146
- const inst = new OpenAIOAuthProvider({});
179
+ const Ctor = providerCtors.get('openai-oauth');
180
+ if (!Ctor) return undefined;
181
+ const inst = new Ctor({});
147
182
  providers.set(name, inst);
148
183
  return inst;
149
184
  }
150
185
  if (name === 'grok-oauth' && hasGrokOAuthCredentials()) {
151
- const inst = new GrokOAuthProvider({});
186
+ const Ctor = providerCtors.get('grok-oauth');
187
+ if (!Ctor) return undefined;
188
+ const inst = new Ctor({});
152
189
  providers.set(name, inst);
153
190
  return inst;
154
191
  }
@@ -173,7 +210,7 @@ export function getAllProviders() {
173
210
  }
174
211
  // Background catalog warm-up. Each provider's listModels() either hits its
175
212
  // own cached model list (no-op) or fires a single HTTP refresh. Called from
176
- // agent.init() after providers are registered so the first bridge LLM call
213
+ // agent.init() after providers are registered so the first agent dispatch call
177
214
  // (e.g. cycle1 on session start) does not pay the catalog refresh latency
178
215
  // inline. Fire-and-forget: failures are logged inside each provider.
179
216
  export function warmupCatalogs() {
@@ -27,10 +27,10 @@ import {
27
27
 
28
28
  // HTTP statuses considered transient — safe to retry with backoff.
29
29
  // 408 — request timeout
30
- // 429 — rate limit (caller may still respect Retry-After, but the kind
31
- // classification here only signals "retryable"; sub-error in the
32
- // provider can still treat 429 as permanent for quota-exhausted)
33
30
  // 500/502/503/504 — server errors (overload / bad gateway / timeout)
31
+ // 429 is deliberately excluded: rate-limit/quota windows are deterministic
32
+ // for the current call and must surface immediately instead of sleeping until
33
+ // an outer watchdog reports a misleading timeout.
34
34
  const TRANSIENT_STATUSES = new Set([408, 500, 502, 503, 504])
35
35
 
36
36
  // HTTP statuses that mean "permanent: stop retrying, surface to caller".
@@ -44,7 +44,7 @@ const PERMANENT_STATUSES = new Set([400, 404, 405, 410, 415, 422])
44
44
  // payload but no explicit status code — we sniff the text and assign the most
45
45
  // likely HTTP equivalent so the retry layer can use the same rules.
46
46
  const MESSAGE_PATTERNS = [
47
- // OpenAI/Codex sometimes surfaces generic backend failures only as
47
+ // OpenAI OAuth/API sometimes surfaces generic backend failures only as
48
48
  // message text plus a request ID, with no HTTP status on the WS event.
49
49
  { regex: /(?:an error occurred while processing your request|please include the request id)/i, status: 503 },
50
50
  // Overload / transient 5xx — server is asking us to back off. The `\b`
@@ -58,7 +58,7 @@ const MESSAGE_PATTERNS = [
58
58
  // by classifyError because per-call retry won't change the answer (the
59
59
  // window must elapse). Providers that want time-bounded retry should
60
60
  // honor Retry-After on the original response, not loop here.
61
- { regex: /(?:rate ?limit|quota)/i, status: 429 },
61
+ { regex: /(?:rate[_ -]?limit|quota|too many requests|resource exhausted|insufficient_quota|quota_exceeded)/i, status: 429 },
62
62
  // Auth — never retryable from our side.
63
63
  { regex: /\b(?:unauthorized|unauthorised|authentication|not authenticated|token expired|access token|invalid api key)\b/i, status: 401 },
64
64
  { regex: /\b(?:forbidden|permission denied|policy violation)\b/i, status: 403 },
@@ -212,6 +212,13 @@ export function isRetryable(err) {
212
212
  const DEFAULT_BACKOFF_MS = PROVIDER_RETRY_BACKOFF_MS
213
213
  const DEFAULT_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS
214
214
 
215
+ export const MIDSTREAM_BACKOFF_MS = [250, 1000, 2000, 4000]
216
+
217
+ export function midstreamBackoffFor(retryNumber, schedule = MIDSTREAM_BACKOFF_MS) {
218
+ const raw = schedule[Math.min(Math.max(retryNumber, 1), schedule.length) - 1]
219
+ return jitterDelayMs(raw)
220
+ }
221
+
215
222
  export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 'symmetric') {
216
223
  const base = Number(ms) || 0
217
224
  if (base <= 0) return 0
@@ -224,6 +231,264 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
224
231
  return Math.max(0, Math.round(base + offset))
225
232
  }
226
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 / firstMeaningfulTimeout / 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 (state.firstMeaningfulTimeout || err?.firstMeaningfulTimeout) {
298
+ return _allowMidstream('first_meaningful_timeout', attemptIndex, policy)
299
+ }
300
+ if (err?.wsSendFailed || state.wsSendFailed) {
301
+ return _allowMidstream('ws_send_failed', attemptIndex, policy)
302
+ }
303
+ if (!state.sawResponseCreated) {
304
+ const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
305
+ if (closeCode !== 1011 && closeCode !== 1012) return null
306
+ }
307
+ if (state.userAbort) return null
308
+
309
+ if (!err) return null
310
+ const status = Number(err?.httpStatus || 0)
311
+ if (status === 401 || status === 403 || status === 429) return null
312
+ if (status >= 500 && status < 600) {
313
+ return _allowMidstream(`http_${status}`, attemptIndex, policy)
314
+ }
315
+
316
+ const name = err?.name || ''
317
+ if (name === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
318
+ if (name === 'StreamStalledAbortError') return _allowMidstream('stream_stalled', attemptIndex, policy)
319
+
320
+ if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
321
+ if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstream('stream_stalled', attemptIndex, policy)
322
+
323
+ const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0)
324
+ if (closeCode === 1006) return _allowMidstream('ws_1006', attemptIndex, policy)
325
+ if (closeCode === 1011) return _allowMidstream('ws_1011', attemptIndex, policy)
326
+ if (closeCode === 1012) return _allowMidstream('ws_1012', attemptIndex, policy)
327
+ if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null
328
+ if (closeCode === 4000) return _allowMidstream('ws_4000', attemptIndex, policy)
329
+ if (closeCode === 1000 && state.sawResponseCreated && !state.sawCompleted) return _allowMidstream('ws_1000', attemptIndex, policy)
330
+
331
+ const failed = err?.responseFailed || state.responseFailedPayload
332
+ if (failed) {
333
+ try {
334
+ const blob = JSON.stringify(failed).toLowerCase()
335
+ if (blob.includes('stream_disconnected')) return _allowMidstream('response_failed_disconnected', attemptIndex, policy)
336
+ if (blob.includes('network_error')) return _allowMidstream('response_failed_network', attemptIndex, policy)
337
+ if (blob.includes('auth context expired')) return _allowMidstream('response_failed_auth_expired', attemptIndex, policy)
338
+ } catch {}
339
+ }
340
+
341
+ return null
342
+ }
343
+
344
+ // Verbatim relocation of anthropic-oauth's _classifyMidstreamError. `signals`
345
+ // carries sawMessageStart / sawCompleted / emittedToolCall / userAbort /
346
+ // watchdogAbort exactly as before.
347
+ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
348
+ if (attemptIndex >= policy.defaultRetries) return null
349
+ if (state.sawCompleted) return null
350
+ if (!state.sawMessageStart) return null
351
+ if (state.userAbort) return null
352
+ if (state.emittedToolCall) return null
353
+
354
+ if (!err) return null
355
+ const status = Number(err?.httpStatus || 0)
356
+ if (status === 401 || status === 403 || status === 429) return null
357
+
358
+ const name = err?.name || ''
359
+ if (name === 'AgentStallAbortError') return 'agent_stall'
360
+ if (name === 'StreamStalledAbortError') return 'stream_stalled'
361
+ if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall'
362
+ if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled'
363
+
364
+ const code = err?.code || err?.cause?.code || ''
365
+ if (code === 'ECONNRESET') return 'reset'
366
+ if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
367
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns'
368
+
369
+ const msg = String(err?.message || '').toLowerCase()
370
+ if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout'
371
+ if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated'
372
+ if (msg.includes('fetch failed')) return 'fetch_failed'
373
+
374
+ return null
375
+ }
376
+
377
+ // B) Unified transport (WS→HTTP) fallback predicate. Identical deny-order +
378
+ // allow-list to the two former copies; `enabled` replaces the per-provider
379
+ // env-flag check (caller computes the flag and passes it).
380
+ const TRANSPORT_FALLBACK_CLASSIFIERS = new Set([
381
+ 'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
382
+ 'first_byte_timeout', 'first_meaningful_timeout',
383
+ 'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
384
+ 'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
385
+ 'ws_send_failed',
386
+ ])
387
+ const TRANSPORT_FALLBACK_ERRNO = new Set([
388
+ 'EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN',
389
+ 'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE',
390
+ ])
391
+
392
+ export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
393
+ if (!enabled) return false
394
+ if (signal?.aborted) return false
395
+ if (err?.liveTextEmitted === true) return false
396
+ if (err?.emittedToolCall === true || err?.unsafeToRetry === true) return false
397
+ const status = Number(err?.httpStatus || err?.status || 0)
398
+ if (status === 401 || status === 403 || status === 404 || status === 429) return false
399
+ if (status >= 500 && status < 600) return true
400
+ const code = String(err?.code || '')
401
+ if (TRANSPORT_FALLBACK_ERRNO.has(code)) return true
402
+ const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '')
403
+ if (TRANSPORT_FALLBACK_CLASSIFIERS.has(classifier)) return true
404
+ if (/^http_5\d\d$/.test(classifier)) return true
405
+ if (err?.firstByteTimeout) return true
406
+ if (err?.firstMeaningfulTimeout) return true
407
+ const msg = String(err?.message || '')
408
+ return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg)
409
+ }
410
+
411
+ // C) Stream-safety stamp latches. Mirrors openai-oauth-ws's _stampLiveText /
412
+ // _stampTool: once text/tool has been marked, every subsequent throw path
413
+ // re-applies the liveTextEmitted/emittedToolCall + unsafeToRetry markers so
414
+ // no upstream gate can reissue the turn and concatenate attempts.
415
+ export function createStreamSafetyStamps() {
416
+ let textLatched = false
417
+ let toolLatched = false
418
+ const stampText = (e) => {
419
+ if (textLatched && e) { try { e.liveTextEmitted = true; e.unsafeToRetry = true } catch {} }
420
+ return e
421
+ }
422
+ const stampTool = (e) => {
423
+ if (toolLatched && e) { try { e.emittedToolCall = true; e.unsafeToRetry = true } catch {} }
424
+ return e
425
+ }
426
+ return {
427
+ markText() { textLatched = true },
428
+ markTool() { toolLatched = true },
429
+ stampText,
430
+ stampTool,
431
+ stampAll: (e) => stampTool(stampText(e)),
432
+ }
433
+ }
434
+
435
+ const _defaultAbortSleep = (ms) => new Promise((r) => setTimeout(r, ms))
436
+
437
+ // D) Abort-aware sleep (single copy). Resolves after `ms`, or rejects with the
438
+ // signal's reason (or `abortMessage`) the moment the signal aborts. `sleepFn`
439
+ // is the injectable no-signal sleep (test seam); `abortMessage` preserves
440
+ // each caller's prior fallback text when the abort reason is not an Error.
441
+ export async function sleepWithAbort(ms, signal, sleepFn = _defaultAbortSleep, abortMessage = 'sleep aborted') {
442
+ if (!ms) return
443
+ if (!signal) {
444
+ await (sleepFn || _defaultAbortSleep)(ms)
445
+ return
446
+ }
447
+ await new Promise((resolve, reject) => {
448
+ const t = setTimeout(() => {
449
+ try { signal.removeEventListener('abort', onAbort) } catch {}
450
+ resolve()
451
+ }, ms)
452
+ const onAbort = () => {
453
+ clearTimeout(t)
454
+ const reason = signal.reason
455
+ reject(reason instanceof Error ? reason : new Error(abortMessage))
456
+ }
457
+ if (signal.aborted) { onAbort(); return }
458
+ signal.addEventListener('abort', onAbort, { once: true })
459
+ })
460
+ }
461
+
462
+ // E) Handshake classifier (moved here from openai-oauth-ws). Default-deny:
463
+ // anything not recognized as transient returns null. 401/403/404/429 are
464
+ // permanent. Returns 'timeout' | 'reset' | 'dns' | 'refused' | 'network' |
465
+ // 'acquire_timeout' | `http_5xx` | null.
466
+ export function classifyHandshakeError(err) {
467
+ if (!err) return null
468
+ const code = err.code || ''
469
+ const msg = String(err.message || '')
470
+ const status = Number(err.httpStatus || 0)
471
+
472
+ if (status === 401 || status === 403 || status === 404 || status === 429) {
473
+ return null
474
+ }
475
+ if (status >= 500 && status < 600) {
476
+ return `http_${status}`
477
+ }
478
+
479
+ if (code === 'ECONNRESET') return 'reset'
480
+ if (code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA') return 'dns'
481
+ if (code === 'ECONNREFUSED') return 'refused'
482
+ if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout'
483
+ if (code === 'EWSACQUIRETIMEOUT') return 'acquire_timeout'
484
+ if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'EPIPE') return 'network'
485
+
486
+ if (/opening handshake has timed out/i.test(msg)) return 'timeout'
487
+ if (/socket hang up/i.test(msg)) return 'reset'
488
+
489
+ return null
490
+ }
491
+
227
492
  /**
228
493
  * Run an async function with exponential-backoff retry on transient errors.
229
494
  *
@@ -296,13 +561,17 @@ export async function withRetry(fn, opts = {}) {
296
561
  }
297
562
  lastErr = caught
298
563
  populateHttpStatusFromMessage(caught)
299
- const retryAfterMs = retryAfterMsFromError(caught)
300
564
  const status = Number(caught?.httpStatus || caught?.status || caught?.response?.status || 0)
301
565
  const kind = classifyError(caught)
302
- const retryableRateLimit = status === 429 && retryAfterMs != null
303
- if (kind !== 'transient' && !retryableRateLimit) throw caught
566
+ const unsafeToRetry = caught?.unsafeToRetry === true
567
+ || caught?.providerQuota === true
568
+ || caught?.quotaExceeded === true
569
+ if (unsafeToRetry) throw caught
570
+ if (status === 429) throw caught
571
+ if (kind !== 'transient') throw caught
304
572
  // Last attempt failed transiently — propagate to caller.
305
573
  if (attempt === maxAttempts - 1) throw caught
574
+ const retryAfterMs = retryAfterMsFromError(caught)
306
575
  if (retryAfterMs != null) {
307
576
  nextDelayMs = Math.max(0, Math.min(retryAfterMs, maxRetryAfterMs))
308
577
  nextDelayReason = 'retry-after'
@@ -316,7 +585,7 @@ export async function withRetry(fn, opts = {}) {
316
585
  }
317
586
 
318
587
  function _sleepWithAbort(ms, signal) {
319
- return new Promise((resolve) => {
588
+ return new Promise((resolve, reject) => {
320
589
  let onAbort = null
321
590
  const t = setTimeout(() => {
322
591
  if (signal && onAbort) {
@@ -325,8 +594,17 @@ function _sleepWithAbort(ms, signal) {
325
594
  resolve()
326
595
  }, ms)
327
596
  if (!signal) return
328
- if (signal.aborted) { clearTimeout(t); resolve(); return }
329
- onAbort = () => { clearTimeout(t); resolve() }
597
+ if (signal.aborted) {
598
+ clearTimeout(t)
599
+ const reason = signal.reason
600
+ reject(reason instanceof Error ? reason : new Error('sleep aborted'))
601
+ return
602
+ }
603
+ onAbort = () => {
604
+ clearTimeout(t)
605
+ const reason = signal.reason
606
+ reject(reason instanceof Error ? reason : new Error('sleep aborted'))
607
+ }
330
608
  signal.addEventListener('abort', onAbort, { once: true })
331
609
  })
332
610
  }
@@ -1,11 +1,44 @@
1
- let routeMeta;
1
+ let routeMeta = null;
2
+ let routeMetaPromise = null;
2
3
 
3
- try {
4
- routeMeta = await import('../../../../vendor/statusline/src/gateway/route-meta.mjs');
5
- } catch {
6
- routeMeta = await import('../../../gateway/route-meta.mjs');
4
+ async function loadRouteMetaModule() {
5
+ if (routeMeta) return routeMeta;
6
+ if (!routeMetaPromise) {
7
+ routeMetaPromise = import('../../../../vendor/statusline/src/gateway/route-meta.mjs').then((mod) => {
8
+ routeMeta = mod;
9
+ return mod;
10
+ });
11
+ }
12
+ try {
13
+ return await routeMetaPromise;
14
+ } catch (err) {
15
+ routeMetaPromise = null;
16
+ routeMeta = null;
17
+ throw err;
18
+ }
7
19
  }
8
20
 
9
- export const buildGatewayLimits = routeMeta.buildGatewayLimits;
10
- export const recordGatewayUsageEvent = routeMeta.recordGatewayUsageEvent;
11
- export const summarizeGatewayUsage = routeMeta.summarizeGatewayUsage;
21
+ /** Await before calling the sync exports when route-meta may not be ready yet. */
22
+ export async function ensureStatuslineRouteMetaLoaded() {
23
+ return loadRouteMetaModule();
24
+ }
25
+
26
+ function requireRouteMeta() {
27
+ if (!routeMeta) {
28
+ void loadRouteMetaModule().catch(() => {});
29
+ throw new Error('statusline route-meta not loaded');
30
+ }
31
+ return routeMeta;
32
+ }
33
+
34
+ export function buildGatewayLimits(routeInfo, providerOut = null, usageSnapshot = null) {
35
+ return requireRouteMeta().buildGatewayLimits(routeInfo, providerOut, usageSnapshot);
36
+ }
37
+
38
+ export function recordGatewayUsageEvent(summary) {
39
+ return requireRouteMeta().recordGatewayUsageEvent(summary);
40
+ }
41
+
42
+ export function summarizeGatewayUsage(routeInfo, providerOut, compact = null, durationMs = null) {
43
+ return requireRouteMeta().summarizeGatewayUsage(routeInfo, providerOut, compact, durationMs);
44
+ }