mixdog 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/package.json +1 -1
  2. package/scripts/gate-local.mjs +19 -3
  3. package/scripts/model-catalog-audit.mjs +1 -15
  4. package/src/agents/reviewer/AGENT.md +3 -3
  5. package/src/agents/reviewer/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +1 -0
  7. package/src/repl/slash-commands.mjs +16 -1
  8. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  10. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
  11. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
  12. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
  14. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  15. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  16. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  17. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  18. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  19. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  20. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  21. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
  23. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  25. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  27. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  29. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  38. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  43. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  45. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  47. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  48. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  50. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  52. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  54. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  55. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  56. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
  57. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  59. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  60. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  65. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  66. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  67. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  71. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  72. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
  89. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  90. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  92. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  93. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  96. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  97. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  98. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  99. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  100. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  101. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  103. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  104. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  105. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  106. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  107. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  108. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
  109. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  110. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  111. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  113. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  114. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  115. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  116. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  117. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  118. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  119. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
  120. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  121. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  123. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  124. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
  125. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
  126. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  130. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  131. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  132. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  133. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  134. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  140. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  141. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  142. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  143. package/src/runtime/office/README.md +1 -1
  144. package/src/runtime/office/shared/values.mjs +0 -5
  145. package/src/runtime/shared/background-tasks.mjs +92 -14
  146. package/src/runtime/shared/err-text.mjs +25 -0
  147. package/src/runtime/shared/error-presentation.mjs +159 -19
  148. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  149. package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
  150. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  151. package/src/runtime/shared/llm/model-catalog.mjs +53 -40
  152. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  153. package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
  154. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  155. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  156. package/src/runtime/shared/model-display.mjs +13 -0
  157. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  158. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  159. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  160. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  161. package/src/runtime/shared/turn-changes.mjs +447 -219
  162. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  163. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  164. package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
  165. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  166. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  167. package/src/session-runtime/boot/apis.mjs +6 -0
  168. package/src/session-runtime/boot/infrastructure.mjs +6 -0
  169. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  170. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  171. package/src/session-runtime/mcp-glue/connect.mjs +29 -9
  172. package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
  173. package/src/session-runtime/mcp-glue.mjs +3 -0
  174. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  175. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  176. package/src/session-runtime/model-route/set-route.mjs +83 -15
  177. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  178. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  179. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  180. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  181. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  182. package/src/session-runtime/resource-extension-sync.mjs +4 -0
  183. package/src/session-runtime/resource-mcp-api.mjs +98 -2
  184. package/src/session-runtime/runtime-review-api.mjs +13 -0
  185. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  186. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  187. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  188. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  189. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  190. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  191. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  192. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  193. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  194. package/src/standalone/daemon-drain-policy.mjs +8 -0
  195. package/src/standalone/daemon-telemetry.mjs +55 -0
  196. package/src/standalone/daemon.mjs +3 -7
  197. package/src/standalone/session-protocol.mjs +7 -0
  198. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  199. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  200. package/src/standalone/session-service/agent-tree.mjs +12 -0
  201. package/src/standalone/session-service/quota-resume.mjs +8 -0
  202. package/src/standalone/session-service/retention/busy.mjs +15 -1
  203. package/src/standalone/session-service-api.mjs +5 -0
  204. package/src/standalone/session-service.mjs +1 -0
  205. package/src/tui/app/app-view.jsx +1 -0
  206. package/src/tui/app/create-app-pickers.mjs +4 -0
  207. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  208. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  209. package/src/tui/app/extension-pickers.mjs +2 -1
  210. package/src/tui/app/model-options.mjs +11 -16
  211. package/src/tui/app/slash-commands.mjs +7 -2
  212. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  213. package/src/tui/components/StatusLine.jsx +2 -0
  214. package/src/tui/components/status-line/local-line.mjs +5 -2
  215. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  216. package/src/tui/dist/index.mjs +816 -43
  217. package/src/tui/session/quota-resume.mjs +46 -14
  218. package/src/tui/session/session-api/commands.mjs +33 -24
  219. package/src/tui/session/session-api/extensions.mjs +22 -0
  220. package/src/tui/session/session-api/integrations.mjs +7 -0
  221. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  222. package/src/tui/session/session-api/routes.mjs +71 -7
  223. package/src/tui/session/session-api/settings.mjs +5 -14
  224. package/src/tui/session/session-flow/drain.mjs +7 -1
  225. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  226. package/src/tui/session/turn.mjs +38 -15
  227. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  228. package/src/ui/statusline-agents.mjs +15 -4
  229. package/src/ui/statusline.mjs +7 -3
  230. package/src/ui/streaming-markdown-heal.mjs +34 -3
  231. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  232. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  233. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -10,7 +10,8 @@ import {
10
10
  incompleteReasonFromEvent,
11
11
  isMaxOutputIncompleteReason,
12
12
  } from '../../lib/responses-terminal-fields.mjs';
13
- import { LABEL, toolInputPending } from '../response-state.mjs';
13
+ import { applyErrorContract, markContentFilter } from '../../openai-error-contract.mjs';
14
+ import { toolInputPending } from '../response-state.mjs';
14
15
  import { normalizeWsUsage } from '../../openai-ws-response-state/usage.mjs';
15
16
 
16
17
  // Typed status only — nothing is synthesized from text. The frame itself is
@@ -19,11 +20,14 @@ import { normalizeWsUsage } from '../../openai-ws-response-state/usage.mjs';
19
20
  function wireFailureError(message, event, detail, typedSources) {
20
21
  const err = new Error(message);
21
22
  err.responseFailed = event;
22
- const typed = typedStatusFrom(...typedSources);
23
- if (typed) err.httpStatus = typed;
24
23
  const code = detail?.code ?? detail?.type ?? null;
25
24
  if (typeof code === 'string' && code) err.providerErrorCode = code;
26
- return err;
25
+ return applyErrorContract(err, {
26
+ status: typedStatusFrom(...typedSources),
27
+ code: typeof code === 'string' ? code : undefined,
28
+ error: detail,
29
+ event,
30
+ });
27
31
  }
28
32
 
29
33
  export function createTerminalFrameEvents({ state, items, text, outcome, meaningful }) {
@@ -89,7 +93,7 @@ export function createTerminalFrameEvents({ state, items, text, outcome, meaning
89
93
  absorbTerminalResponse(event.response || {});
90
94
  const reason = incompleteReasonFromEvent(event);
91
95
  if (!isMaxOutputIncompleteReason(reason)) {
92
- const err = new Error(`${LABEL} ${frame}: ${reason}`);
96
+ const err = markContentFilter(new Error(`${state.label} ${frame}: ${reason}`), reason);
93
97
  // Still billed: the provider-boundary accounting records reported usage.
94
98
  if (state.usage) {
95
99
  err.partialUsage = state.usage;
@@ -98,7 +102,7 @@ export function createTerminalFrameEvents({ state, items, text, outcome, meaning
98
102
  throw err;
99
103
  }
100
104
  if (toolInputPending(state)) {
101
- const err = outcome.stampToolSafety(new Error(`${LABEL} ${frame} (max_output_tokens) with tool call in flight`));
105
+ const err = outcome.stampToolSafety(new Error(`${state.label} ${frame} (max_output_tokens) with tool call in flight`));
102
106
  err.streamStalled = true;
103
107
  err.pendingToolUse = true;
104
108
  err.partialContent = state.content;
@@ -119,9 +123,12 @@ export function createTerminalFrameEvents({ state, items, text, outcome, meaning
119
123
  setEndTurn(event);
120
124
  } else if (event.response.status === 'failed') {
121
125
  const msg = event.response?.error?.message || 'response.done failed';
122
- const err = new Error(`${LABEL} response.done failed: ${msg}`);
123
- const typed = typedStatusFrom(event.response?.error, event.error, event);
124
- if (typed) err.httpStatus = typed;
126
+ const err = new Error(`${state.label} response.done failed: ${msg}`);
127
+ applyErrorContract(err, {
128
+ status: typedStatusFrom(event.response?.error, event.error, event),
129
+ error: event.response?.error || event.error || null,
130
+ event,
131
+ });
125
132
  throw withFailedUsage(err, event);
126
133
  } else if (event.response.status === 'incomplete') {
127
134
  onIncomplete(event, 'response.done incomplete');
@@ -131,7 +138,7 @@ export function createTerminalFrameEvents({ state, items, text, outcome, meaning
131
138
  const failedFrameError = (event) =>
132
139
  withFailedUsage(
133
140
  wireFailureError(
134
- `${LABEL} response.failed: ${event.response?.error?.message || event.error?.message || event.message || 'response.failed'}`,
141
+ `${state.label} response.failed: ${event.response?.error?.message || event.error?.message || event.message || 'response.failed'}`,
135
142
  event,
136
143
  event.response?.error || event.error || null,
137
144
  [event.response?.error, event.error, event]
@@ -142,7 +149,7 @@ export function createTerminalFrameEvents({ state, items, text, outcome, meaning
142
149
  // Same wire-error contract as response.failed.
143
150
  const errorFrameError = (event) =>
144
151
  wireFailureError(
145
- `${LABEL} error: ${event.message || event.error?.message || 'unknown'}`,
152
+ `${state.label} error: ${event.message || event.error?.message || 'unknown'}`,
146
153
  event,
147
154
  { code: event.error?.code ?? event.error?.type ?? event.code ?? null },
148
155
  [event.error, event]
@@ -8,7 +8,6 @@ import { traceAgentSse, traceAgentUsage } from '../../agent-trace.mjs';
8
8
  import { dedupeToolCallList } from '../lib/leaked-toolcall.mjs';
9
9
  import { _displayCodexModel } from '../openai-codex-model.mjs';
10
10
  import { createProviderReplay } from '../lib/provider-replay.mjs';
11
- import { LABEL } from './response-state.mjs';
12
11
  import { createResponseStamps } from './response-stamps.mjs';
13
12
 
14
13
  export function createResponseOutcome({ state }) {
@@ -50,7 +49,7 @@ export function createResponseOutcome({ state }) {
50
49
  throw stampOutcome(
51
50
  stampToolSafety(
52
51
  new Error(
53
- `${LABEL} function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`
52
+ `${state.label} function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`
54
53
  )
55
54
  )
56
55
  );
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { stampStreamOutcome, STREAM_TRANSPORTS } from '../lib/stream-outcome.mjs';
11
11
  import { createProviderReplay } from '../lib/provider-replay.mjs';
12
- import { LABEL, toolInputPending } from './response-state.mjs';
12
+ import { toolInputPending } from './response-state.mjs';
13
13
 
14
14
  export function createResponseStamps({ state }) {
15
15
  const stampToolSafety = (err) => {
@@ -111,7 +111,7 @@ export function createResponseStamps({ state }) {
111
111
  const err = attachStreamPartial(
112
112
  stampToolSafety(
113
113
  new Error(
114
- `${LABEL} ended before response.completed (text=${state.content.length} chars, toolCalls=${state.toolCalls.length})`
114
+ `${state.label} ended before response.completed (text=${state.content.length} chars, toolCalls=${state.toolCalls.length})`
115
115
  )
116
116
  )
117
117
  );
@@ -8,10 +8,9 @@
8
8
  import { createActiveToolItemTracker } from '../tool-stream-state.mjs';
9
9
  import { createToolCallDedupe } from '../lib/leaked-toolcall.mjs';
10
10
 
11
- export const LABEL = 'OpenAI OAuth HTTP fallback';
12
-
13
- export function createResponseState() {
11
+ export function createResponseState(label) {
14
12
  return {
13
+ label,
15
14
  content: '',
16
15
  model: '',
17
16
  responseId: '',
@@ -26,8 +26,8 @@ import { createResponseEvents } from './openai-http-sse-response-state/response-
26
26
  * @param {(kind: string) => void} deps.meaningful semantic progress (re-arms the idle watchdog)
27
27
  * @param {() => void} deps.onServerEvent first-event → semantic-idle handover
28
28
  */
29
- export function createHttpSseResponseState({ body, onToolCall, onTextDelta, meaningful, onServerEvent }) {
30
- const state = createResponseState();
29
+ export function createHttpSseResponseState({ body, onToolCall, onTextDelta, meaningful, onServerEvent, label }) {
30
+ const state = createResponseState(label);
31
31
  const outcome = createResponseOutcome({ state });
32
32
  const items = createResponseItems({ state, onToolCall });
33
33
  const text = createTextRelay({ state, body, onTextDelta, meaningful, emitToolCall: items.emitToolCall });
@@ -20,8 +20,6 @@ import {
20
20
  streamStalledError,
21
21
  } from '../stall-policy.mjs';
22
22
 
23
- const LABEL = 'OpenAI OAuth HTTP fallback';
24
-
25
23
  function positiveOverride(value) {
26
24
  const ms = Number(value);
27
25
  return Number.isFinite(ms) && ms > 0 ? ms : 0;
@@ -34,7 +32,7 @@ function positiveOverride(value) {
34
32
  * @param {{ signal?: AbortSignal, cleanup(): void }} deps.totalTimeout
35
33
  * @param {() => object} deps.stallPartial streamed partial state attached to a semantic-idle stall
36
34
  */
37
- export function createHttpSseWatchdogs({ opts, reader, totalTimeout, stallPartial }) {
35
+ export function createHttpSseWatchdogs({ opts, reader, totalTimeout, stallPartial, label: LABEL }) {
38
36
  let abortReason = null;
39
37
  let pendingReadReject = null;
40
38
  const rejectPendingRead = (err) => {
@@ -13,6 +13,9 @@ import { codexCatalogHas } from './openai-oauth-catalog.mjs';
13
13
  import { _displayCodexModel } from './openai-codex-model.mjs';
14
14
  import { _envFlag, _shouldUseOpenAIHttpFallback } from './openai-oauth-http-sse.mjs';
15
15
  import { isWsMessageTooBigClose } from './retry-classifier.mjs';
16
+ import { waitForRetryDeadline } from './openai-error-contract.mjs';
17
+ import { isReplaySafe } from './lib/stream-outcome.mjs';
18
+ import { noteErrorUsage } from './lib/note-error-usage.mjs';
16
19
 
17
20
  function openAiOAuthHandshakeErrorPolicy({ status }) {
18
21
  if (Number(status) === 404) {
@@ -130,6 +133,17 @@ export function createOpenAiOAuthDispatch({
130
133
  });
131
134
  } catch {}
132
135
  };
136
+ // A 401 is recoverable once per send, and only while nothing the turn
137
+ // produced could be duplicated by reissuing it.
138
+ const canReissueAfter401 = (err) =>
139
+ Number(err?.httpStatus || err?.status || 0) === 401 &&
140
+ authState.authRefreshed !== true &&
141
+ !externalSignal?.aborted &&
142
+ err?.liveTextEmitted !== true &&
143
+ err?.emittedToolCall !== true &&
144
+ err?.toolCallEmitted !== true &&
145
+ err?.unsafeToRetry !== true &&
146
+ isReplaySafe(err);
133
147
  const dispatchHttp = async (reason, originalErr = null, { sticky = false } = {}) => {
134
148
  // Transport switching is a session decision, not an HTTP-success
135
149
  // side effect. Persist it before the fallback can fail or abort.
@@ -158,9 +172,10 @@ export function createOpenAiOAuthDispatch({
158
172
  if (!process.env.MIXDOG_QUIET_PROVIDER_LOG)
159
173
  process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
160
174
  }
161
- let result;
162
- try {
163
- result = await sendHttp({
175
+ // The server's retry deadline outlives a transport switch.
176
+ if (originalErr && reason !== 'forced') await waitForRetryDeadline(originalErr, externalSignal, opts._sleepFn);
177
+ const sendHttpOnce = () =>
178
+ sendHttp({
164
179
  auth: authState.tokens,
165
180
  body,
166
181
  opts,
@@ -175,6 +190,22 @@ export function createOpenAiOAuthDispatch({
175
190
  useModel,
176
191
  fetchFn: opts._fetchFn,
177
192
  });
193
+ let result;
194
+ try {
195
+ try {
196
+ result = await sendHttpOnce();
197
+ } catch (firstErr) {
198
+ if (!canReissueAfter401(firstErr)) throw firstErr;
199
+ // Same bounded, replay-safe recovery the WS path applies: one forced
200
+ // refresh per send, one reissue of the identical request.
201
+ noteErrorUsage(firstErr);
202
+ traceTransportError(firstErr, 'primary', 'http');
203
+ process.stderr.write('[openai-oauth-http] 401 — forcing refresh and retrying once over HTTP/SSE\n');
204
+ authState.authRefreshed = true;
205
+ provider._refreshFallbackUntil = 0;
206
+ authState.tokens = await provider.ensureAuth({ forceRefresh: true, reason: '401' });
207
+ result = await sendHttpOnce();
208
+ }
178
209
  } catch (httpErr) {
179
210
  // The current transport owns the terminal decision, including
180
211
  // auth, rate limits, cancellation and output-safety markers.
@@ -16,6 +16,7 @@ import { CODEX_OAUTH_ORIGINATOR } from './openai-codex-endpoints.mjs';
16
16
  import { parseProviderJsonBatch } from './stream-json-pool.mjs';
17
17
  import { envFlag as _envFlag } from '../../../shared/env.mjs';
18
18
  import { activateCodexTurnState } from './openai-turn-state.mjs';
19
+ import { httpLabel } from './openai-error-contract.mjs';
19
20
  import { openHttpSseResponse } from './openai-http-sse-request.mjs';
20
21
  import { createHttpSseWatchdogs } from './openai-http-sse-watchdogs.mjs';
21
22
  import { createHttpSseResponseState } from './openai-http-sse-response-state.mjs';
@@ -165,6 +166,7 @@ export async function sendViaHttpSse({
165
166
  onStageChange?.('streaming');
166
167
  } catch {}
167
168
  const sseStartedAt = Date.now();
169
+ const label = httpLabel(auth);
168
170
  const reader = response.body.getReader();
169
171
  const decoder = new TextDecoder();
170
172
  let ttftMs = null;
@@ -173,6 +175,7 @@ export async function sendViaHttpSse({
173
175
  reader,
174
176
  totalTimeout,
175
177
  stallPartial: () => state.stallPartial(),
178
+ label,
176
179
  });
177
180
  const meaningful = (kind = 'semantic') => {
178
181
  if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
@@ -187,6 +190,7 @@ export async function sendViaHttpSse({
187
190
  onTextDelta,
188
191
  meaningful,
189
192
  onServerEvent: watchdogs.noteServerEvent,
193
+ label,
190
194
  });
191
195
  const foldFrames = async (text) => {
192
196
  const parsed = _sseEventsFromBuffer(text);
@@ -202,7 +206,7 @@ export async function sendViaHttpSse({
202
206
  if (totalTimeout.signal?.aborted) {
203
207
  watchdogs.clearSemanticIdle();
204
208
  const reason = totalTimeout.signal.reason;
205
- throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
209
+ throw reason instanceof Error ? reason : new Error(`${label} aborted`);
206
210
  }
207
211
  const abortReason = watchdogs.abortReason();
208
212
  if (abortReason) throw abortReason;
@@ -189,6 +189,7 @@ export function forgetOpenAIOAuthCredentials() {
189
189
  }
190
190
 
191
191
  // --- Token refresh ---
192
+ const PERMANENT_REFRESH_FAILURE_RE = /invalid_grant|revoked|reused|refresh_token_(?:expired|invalidated)/i;
192
193
  async function exchangeRefreshToken(refreshToken, previousIdentity = null) {
193
194
  const controller = new AbortController();
194
195
  const timeout = setTimeout(() => controller.abort(), TOKEN_REFRESH_TIMEOUT_MS);
@@ -214,10 +215,17 @@ async function exchangeRefreshToken(refreshToken, previousIdentity = null) {
214
215
  // CLI's single-use lineage) from transient failures, so the caller can
215
216
  // re-read disk and retry once with a newer token instead of
216
217
  // collapsing every failure to a generic null.
217
- if (res.status === 400 || res.status === 401 || /invalid_grant|revoked|reused/i.test(text)) {
218
+ // Only a CONFIRMED permanent refusal sets reauthRequired: a 401, or a
219
+ // typed refresh-token code. A bare 400/5xx/429 is not evidence the
220
+ // refresh token is dead.
221
+ if (res.status === 401 || PERMANENT_REFRESH_FAILURE_RE.test(text)) {
218
222
  throw Object.assign(new Error(`OpenAI OAuth token refresh ${res.status} (invalid_grant)`), {
219
223
  isInvalidGrant: true,
220
224
  reauthRequired: true,
225
+ status: res.status,
226
+ httpStatus: res.status,
227
+ code: 'invalid_grant',
228
+ headers: res.headers,
221
229
  });
222
230
  }
223
231
  return null;
@@ -312,7 +320,19 @@ async function refreshUnderLock({ currentToken, force, reason, startingTokens })
312
320
  process.stderr.write(`[openai-oauth] Refresh failed (${msg}); using still-valid current token\n`);
313
321
  return { tokens: reusable, coastOnCurrent: true };
314
322
  }
315
- throw new Error(`OpenAI OAuth token refresh failed (${msg}). Re-authenticate via provider login.`);
323
+ const failure = new Error(`OpenAI OAuth token refresh failed (${msg}). Re-authenticate via provider login.`);
324
+ // The wrapper must not hide a confirmed permanent refusal from the account pool.
325
+ if (err?.reauthRequired === true) {
326
+ Object.assign(failure, {
327
+ reauthRequired: true,
328
+ isInvalidGrant: true,
329
+ status: err.status,
330
+ httpStatus: err.httpStatus,
331
+ code: err.code,
332
+ headers: err.headers,
333
+ });
334
+ }
335
+ throw failure;
316
336
  }
317
337
  }
318
338
 
@@ -86,6 +86,9 @@ export class OpenAIOAuthProvider {
86
86
  // so input alone is the context footprint. See registry.mjs.
87
87
  static inputExcludesCache = false;
88
88
  name = 'openai-oauth';
89
+ // Loop retry policy (send-with-recovery loopRetryPolicy): a connect failure waits for
90
+ // the network, and a content_filter refusal is replayed under the budget.
91
+ retryPolicy = Object.freeze({ unboundedConnectionRetry: true, contentFilterRetryable: true });
89
92
  tokens = null;
90
93
  _refreshFallbackUntil = 0;
91
94
  // Sticky transport fallback is isolated by the WS pool/session key. A
@@ -277,6 +280,8 @@ export class OpenAIOAuthProvider {
277
280
  process.stderr.write(`[agent-trace] provider-${status}-retry attempt=1\n`);
278
281
  }
279
282
  this._refreshFallbackUntil = 0;
283
+ // One forced refresh per send, shared with the HTTP/SSE dispatch.
284
+ authState.authRefreshed = true;
280
285
  authState.tokens = await this.ensureAuth({ forceRefresh: true, reason: String(status) });
281
286
  try {
282
287
  const result = await dispatch.dispatchWs(true);
@@ -1,6 +1,7 @@
1
1
  // Transcript lowering is independent of request/turn metadata. A retained
2
2
  // message must have the same wire representation in every subsequent request.
3
3
  import { providerReplayItems } from './lib/provider-replay.mjs';
4
+ import { responsesEffortUpdates } from './effort-configuration.mjs';
4
5
  import { ensureResponsesCallOutputs } from './lib/wire-pairing.mjs';
5
6
  import { normalizeContentForOpenAIResponses, splitToolContentForOpenAIResponses } from './media-normalization.mjs';
6
7
  import {
@@ -156,21 +157,16 @@ export function convertMessagesToResponsesInput(messages, opts = {}) {
156
157
  if (!pendingToolMedia.length) return;
157
158
  out.push(wireMessage('user', pendingToolMedia.splice(0)));
158
159
  };
159
- // An update on a tool result applies after its whole batch; one on a user
160
- // message before it. Adjacent updates are rejected, so a batch update that
161
- // a user message directly follows yields to that message's own.
162
- let afterBatch = null;
160
+ const effortUpdates = responsesEffortUpdates(opts.effortProjection);
163
161
  for (const m of messages) {
164
162
  if (!m || m.role === 'system') continue;
165
- const changedEffort = opts.effortProjection?.updates.get(m);
163
+ const updates = effortUpdates.before(m);
166
164
  if (m.role === 'tool') {
167
165
  pushToolResult(m, { out, pendingToolMedia, pendingToolLoads, customToolCallNameById, nativeSearchCalls, opts });
168
- if (changedEffort) afterBatch = changedEffort;
169
166
  continue;
170
167
  }
171
168
  flushToolMedia();
172
- if (changedEffort || afterBatch) out.push({ type: 'configuration_update', reasoning: { effort: changedEffort || afterBatch } });
173
- afterBatch = null;
169
+ out.push(...updates);
174
170
  // Preserve original assistant phases and item order even when encrypted
175
171
  // reasoning is explicitly disabled. Never replay another provider's data.
176
172
  const orderedReplay =
@@ -212,7 +208,7 @@ export function convertMessagesToResponsesInput(messages, opts = {}) {
212
208
  pushReasoningItems(m, 'after');
213
209
  }
214
210
  flushToolMedia();
215
- if (afterBatch) out.push({ type: 'configuration_update', reasoning: { effort: afterBatch } });
211
+ out.push(...effortUpdates.end());
216
212
  // Wire-level pairing guard: replay envelopes can carry a call whose
217
213
  // result never committed (cancel/abort). The provider hard-rejects the
218
214
  // unpaired call, so synthesize the missing outputs here.
@@ -0,0 +1,9 @@
1
+ // OpenAI retry schedule: 200ms base, factor 2, ±10%.
2
+ // Provider-owned values; the shared retry core only takes them as options.
3
+ // CODEX_STREAM_BACKOFF_MS — indexed by the 1-based retry number
4
+ // (midstreamBackoffFor).
5
+ // CODEX_REQUEST_RETRY_BACKOFF_MS — leading duplicate for withRetry's
6
+ // sleep-before-attempt index.
7
+ export const CODEX_STREAM_BACKOFF_MS = Object.freeze([200, 400, 800, 1600, 3200]);
8
+ export const CODEX_REQUEST_RETRY_BACKOFF_MS = Object.freeze([200, ...CODEX_STREAM_BACKOFF_MS]);
9
+ export const CODEX_RETRY_JITTER_RATIO = 0.1;
@@ -9,6 +9,11 @@
9
9
  import { appendFileSync, chmodSync } from 'node:fs';
10
10
  import { errText } from '../../../shared/err-text.mjs';
11
11
  import { _cfCookieCapture, _formatRedactedHeaders } from './openai-ws-headers.mjs';
12
+ import { httpResponseError } from './openai-error-contract.mjs';
13
+
14
+ // Enough of the handshake body to parse a typed JSON error; diagnostics are
15
+ // truncated separately by the error message.
16
+ const HANDSHAKE_BODY_LIMIT = 16 * 1024;
12
17
 
13
18
  /** CF cookie jar refresh plus the opt-in upgrade-header dump. */
14
19
  export function captureHandshakeUpgrade(auth, res) {
@@ -85,7 +90,7 @@ export function attachHandshakeFailureHandlers({ socket, errLabel, openStart, is
85
90
  const status = res?.statusCode || 0;
86
91
  let body = '';
87
92
  res.on('data', (c) => {
88
- if (body.length < 2048) body += c.toString('utf-8');
93
+ if (body.length < HANDSHAKE_BODY_LIMIT) body += c.toString('utf-8');
89
94
  });
90
95
  res.on('end', () => {
91
96
  if (process.env.MIXDOG_DEBUG_AGENT) {
@@ -96,13 +101,16 @@ export function attachHandshakeFailureHandlers({ socket, errLabel, openStart, is
96
101
  try {
97
102
  socket.terminate();
98
103
  } catch {}
99
- settle(
100
- false,
101
- Object.assign(new Error(`${errLabel} handshake ${status}: ${body.slice(0, 200)}`), {
102
- httpStatus: status,
103
- httpBody: body,
104
- })
105
- );
104
+ // Status, headers and the parsed body ride on the error; only the
105
+ // message is truncated.
106
+ const err = httpResponseError({
107
+ label: `${errLabel} handshake`,
108
+ status,
109
+ headers: res.headers,
110
+ text: body,
111
+ });
112
+ err.httpBody = body;
113
+ settle(false, err);
106
114
  });
107
115
  });
108
116
  }
@@ -3,7 +3,6 @@
3
3
  * WS send, plus the surface/backoff primitives both failure resolvers use.
4
4
  */
5
5
  import { performance } from 'node:perf_hooks';
6
- import { retryAfterMsFromError } from '../retry-classifier.mjs';
7
6
  import { _sleepWithAbort, MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT, midstreamBackoffFor, tag } from './policy.mjs';
8
7
 
9
8
  export function createAttemptContext(deps) {
@@ -35,7 +34,7 @@ export function createAttemptContext(deps) {
35
34
  const backoff = async (retryNumber, err = null) => {
36
35
  const sleepStart = performance.now();
37
36
  try {
38
- await _sleepWithAbort(retryAfterMsFromError(err) ?? midstreamBackoffFor(retryNumber), externalSignal, sleepFn);
37
+ await _sleepWithAbort(midstreamBackoffFor(retryNumber, err), externalSignal, sleepFn);
39
38
  } catch (sleepErr) {
40
39
  sendSpan.retryBackoffMs += performance.now() - sleepStart;
41
40
  sendSpan.emit('error');
@@ -2,12 +2,13 @@
2
2
  * policy.mjs — the WS send retry budget, its backoff curves and the
3
3
  * classification wrappers shared by the handshake and stream resolvers.
4
4
  */
5
+ import { CODEX_RETRY_JITTER_RATIO, CODEX_STREAM_BACKOFF_MS } from '../openai-retry-policy.mjs';
5
6
  import {
6
7
  classifyHandshakeError,
7
8
  classifyMidstreamError,
8
9
  isWsMessageTooBigClose,
9
- jitterDelayMs,
10
10
  MIDSTREAM_RETRY_POLICY,
11
+ midstreamBackoffFor as sharedMidstreamBackoffFor,
11
12
  sleepWithAbort,
12
13
  } from '../retry-classifier.mjs';
13
14
 
@@ -15,23 +16,26 @@ import {
15
16
  // by connect/handshake and pre-output stream failures.
16
17
  export const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries;
17
18
  const MIDSTREAM_DEFAULT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.defaultRetries;
18
- // The reference client uses a 200ms base, factor 2, and symmetric ±10%
19
- // jitter for each of its five stream retries.
20
- const MIDSTREAM_BACKOFF_MS = Object.freeze([200, 400, 800, 1600, 3200]);
21
- const CODEX_RETRY_JITTER_RATIO = 0.1;
19
+ // The stream retry schedule (200ms base, factor 2, ±10%) lives in
20
+ // openai-retry-policy.mjs, shared with the HTTP request loop.
21
+ // Advice-gated retry (OAuth and API-key WS alike): an overload and an untyped 429
22
+ // retry only when the server gave advice.
23
+ const ADVICE_GATES = Object.freeze({
24
+ overloadRetryRequiresAdvice: true,
25
+ generic429RetryRequiresAdvice: true,
26
+ });
22
27
  // Policy object passed to the shared classifyMidstreamError for the WS path.
23
28
  const WS_MIDSTREAM_POLICY = {
24
29
  mode: 'ws',
25
30
  transientCloseRetries: MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries,
26
31
  defaultRetries: MIDSTREAM_RETRY_POLICY.ws.defaultRetries,
32
+ ...ADVICE_GATES,
27
33
  };
28
34
 
29
35
  // Kept for the _acquireWithRetry test seam and non-Codex callers.
30
36
  // sendViaWebSocket deliberately invokes it with maxAttempts:1 so handshake
31
37
  // failures consume the same stream budget as pre-output disconnects.
32
38
  export const HANDSHAKE_MAX_ATTEMPTS = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT + 1;
33
- const HANDSHAKE_BACKOFF_BASE_MS = 200;
34
- const HANDSHAKE_BACKOFF_CAP_MS = 3200;
35
39
  // These statuses are decisions made by the CURRENT attempt. They must never
36
40
  // be replaced by a stale transient from an earlier retry: auth recovery,
37
41
  // transport fallback, and Retry-After handling all depend on the live error.
@@ -47,8 +51,15 @@ export function _mustSurfaceCurrentAttempt(err, externalSignal) {
47
51
  );
48
52
  }
49
53
 
50
- export function _classifyHandshakeError(err, { retry429 = true } = {}) {
51
- return classifyHandshakeError(err, { retry429 });
54
+ export function _classifyHandshakeError(
55
+ err,
56
+ {
57
+ retry429 = true,
58
+ overloadRetryRequiresAdvice = ADVICE_GATES.overloadRetryRequiresAdvice,
59
+ generic429RetryRequiresAdvice = ADVICE_GATES.generic429RetryRequiresAdvice,
60
+ } = {}
61
+ ) {
62
+ return classifyHandshakeError(err, { retry429, overloadRetryRequiresAdvice, generic429RetryRequiresAdvice });
52
63
  }
53
64
 
54
65
  /**
@@ -103,15 +114,15 @@ export function midstreamRetryLimit(classifier) {
103
114
  : MIDSTREAM_DEFAULT_RETRY_LIMIT;
104
115
  }
105
116
 
106
- export function midstreamBackoffFor(retryNumber) {
107
- const raw = MIDSTREAM_BACKOFF_MS[Math.min(Math.max(retryNumber, 1), MIDSTREAM_BACKOFF_MS.length) - 1];
108
- return jitterDelayMs(raw, CODEX_RETRY_JITTER_RATIO);
117
+ // Passing the failure makes server advice replace the step for retryable
118
+ // kinds only (midstreamRetryDelayMs).
119
+ export function midstreamBackoffFor(retryNumber, err = null) {
120
+ return sharedMidstreamBackoffFor(retryNumber, CODEX_STREAM_BACKOFF_MS, err, CODEX_RETRY_JITTER_RATIO);
109
121
  }
110
122
 
111
123
  export function _backoffFor(attempt) {
112
124
  // attempt is 1-based; exponential backoff is capped before jitter.
113
- const raw = HANDSHAKE_BACKOFF_BASE_MS * (1 << (attempt - 1));
114
- return jitterDelayMs(Math.min(raw, HANDSHAKE_BACKOFF_CAP_MS), CODEX_RETRY_JITTER_RATIO);
125
+ return midstreamBackoffFor(attempt);
115
126
  }
116
127
 
117
128
  export const _defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -4,6 +4,7 @@
4
4
  * builder returns a tagged Error the stream loop stores as terminalError.
5
5
  */
6
6
  import { typedStatusFrom } from './retry-classifier.mjs';
7
+ import { applyErrorContract, markContentFilter } from './openai-error-contract.mjs';
7
8
  import { _httpStatusFromWsClose } from './openai-ws-events.mjs';
8
9
 
9
10
  /** A server `event.error` on any frame. Same wire-error contract as
@@ -17,10 +18,9 @@ export function serverErrorEventError(error, event) {
17
18
  try {
18
19
  err.payload = error;
19
20
  err.responseFailed = event;
20
- const typed = typedStatusFrom(error, event);
21
- if (typed) err.httpStatus = typed;
22
21
  const code = error.code ?? error.type ?? null;
23
22
  if (typeof code === 'string' && code) err.providerErrorCode = code;
23
+ applyErrorContract(err, { status: typedStatusFrom(error, event), error, event });
24
24
  } catch {}
25
25
  return err;
26
26
  }
@@ -32,23 +32,27 @@ export function serverErrorEventError(error, event) {
32
32
  export function responseFailedError(event, { label, fallbackMessage, providerErrorCode = false }) {
33
33
  const msg = event.response?.error?.message || event.error?.message || event.message || fallbackMessage;
34
34
  const err = Object.assign(new Error(`${label}: ${msg}`), { responseFailed: event });
35
- const typed = typedStatusFrom(event.response?.error, event.error, event);
36
- if (typed) err.httpStatus = typed;
35
+ const detail = event.response?.error || event.error || null;
37
36
  if (providerErrorCode) {
38
- const detail = event.response?.error || event.error || null;
39
37
  const code = detail?.code ?? detail?.type ?? null;
40
38
  if (typeof code === 'string' && code) err.providerErrorCode = code;
41
39
  }
42
- return err;
40
+ return applyErrorContract(err, {
41
+ status: typedStatusFrom(event.response?.error, event.error, event),
42
+ error: detail,
43
+ event,
44
+ });
43
45
  }
44
46
 
45
47
  /** response.incomplete (or response.done status 'incomplete') for a reason
46
- * other than max_output_tokens. */
48
+ * other than max_output_tokens. A content_filter stop is a typed policy
49
+ * refusal (code 'content_filter'), not an untyped failure. */
47
50
  export function responseIncompleteError(event, reasonStr, label) {
48
- return Object.assign(new Error(`${label}: ${reasonStr}`), {
51
+ const err = Object.assign(new Error(`${label}: ${reasonStr}`), {
49
52
  responseIncomplete: event,
50
53
  incompleteReason: reasonStr,
51
54
  });
55
+ return markContentFilter(err, reasonStr);
52
56
  }
53
57
 
54
58
  /** response.done with a status that is neither completed, failed nor incomplete. */
@@ -63,11 +67,14 @@ export function errorFrameError(event, label) {
63
67
  const errMsg = String(event.message || event.error?.message || 'unknown');
64
68
  const err = new Error(`${label}: ${errMsg}`);
65
69
  err.responseFailed = event;
66
- const typed = typedStatusFrom(event.error, event);
67
- if (typed) err.httpStatus = typed;
68
70
  const code = event.error?.code ?? event.error?.type ?? event.code ?? null;
69
71
  if (typeof code === 'string' && code) err.providerErrorCode = code;
70
- return err;
72
+ return applyErrorContract(err, {
73
+ status: typedStatusFrom(event.error, event),
74
+ code: typeof code === 'string' ? code : undefined,
75
+ error: event.error,
76
+ event,
77
+ });
71
78
  }
72
79
 
73
80
  /** The socket closed before a terminal frame. */