@bitkyc08/opencodex 2.82.0 → 2.84.0

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 (266) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/cursor/catalog.ts +2 -2
  10. package/src/adapters/cursor/effort-map.ts +1 -1
  11. package/src/adapters/devin/live-models.ts +2 -2
  12. package/src/adapters/google-truncation.ts +22 -0
  13. package/src/adapters/google.ts +7 -3
  14. package/src/adapters/ollama-native.ts +47 -15
  15. package/src/adapters/openai-chat/messages.ts +8 -4
  16. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  17. package/src/adapters/openai-responses/passthrough.ts +5 -0
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/request-strips.ts +10 -2
  21. package/src/adapters/qoder/adapter.ts +16 -14
  22. package/src/bridge/errors.ts +26 -1
  23. package/src/bridge.ts +1 -1
  24. package/src/claude/context-windows.ts +57 -4
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/intercept/connect-proxy.ts +10 -3
  27. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  28. package/src/claude/intercept/picker-listener.ts +49 -1
  29. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  30. package/src/claude/intercept/settings.ts +101 -9
  31. package/src/cli/account-api.ts +65 -0
  32. package/src/cli/account-auth.ts +44 -12
  33. package/src/cli/account.ts +41 -5
  34. package/src/cli/capabilities-access-remote.ts +8 -0
  35. package/src/cli/capabilities-base.ts +13 -1
  36. package/src/cli/catalog-command-result.ts +2 -1
  37. package/src/cli/claude.ts +15 -6
  38. package/src/cli/config-command.ts +8 -2
  39. package/src/cli/debug.ts +8 -0
  40. package/src/cli/dispatch.ts +16 -3
  41. package/src/cli/export-command.ts +14 -1
  42. package/src/cli/hub-disable.ts +99 -0
  43. package/src/cli/hub.ts +4 -2
  44. package/src/cli/index.ts +13 -4
  45. package/src/cli/link-child.ts +104 -0
  46. package/src/cli/link-enrollment.ts +96 -0
  47. package/src/cli/link.ts +39 -24
  48. package/src/cli/observe-stream.ts +6 -1
  49. package/src/cli/observe.ts +10 -2
  50. package/src/cli/provider.ts +12 -0
  51. package/src/cli/registry.ts +5 -4
  52. package/src/cli/resolve.ts +1 -1
  53. package/src/cli/runtime-api.ts +27 -2
  54. package/src/cli/status.ts +104 -20
  55. package/src/client/connect.ts +39 -4
  56. package/src/client/hub-client.ts +14 -7
  57. package/src/client/hub-state.ts +25 -14
  58. package/src/client/link-home.ts +248 -0
  59. package/src/client/link-management-relay.ts +61 -0
  60. package/src/client/link-relay.ts +6 -1
  61. package/src/client/link-status.ts +14 -0
  62. package/src/client/machine-api.ts +14 -7
  63. package/src/client/machine-listener.ts +29 -4
  64. package/src/clients/config-export/model-metadata.ts +16 -0
  65. package/src/clients/config-export/zcode-store.ts +46 -11
  66. package/src/clients/config-export/zcode.ts +3 -5
  67. package/src/clients/config-export.ts +47 -17
  68. package/src/clients/omo-role-models.ts +6 -3
  69. package/src/codex/agent-role-models.ts +26 -8
  70. package/src/codex/auth-api/account-list.ts +17 -2
  71. package/src/codex/auth-api/account-selection.ts +17 -7
  72. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  73. package/src/codex/auth-api/routes.ts +5 -1
  74. package/src/codex/auth-context.ts +8 -3
  75. package/src/codex/autostart-health.ts +3 -1
  76. package/src/codex/catalog/bundled.ts +5 -1
  77. package/src/codex/catalog/effort.ts +2 -2
  78. package/src/codex/catalog/metadata.ts +4 -2
  79. package/src/codex/catalog/native-models.ts +17 -3
  80. package/src/codex/catalog/provider-models.ts +25 -0
  81. package/src/codex/desktop-app/darwin.ts +1 -1
  82. package/src/codex/desktop-app/handoff.ts +1 -1
  83. package/src/codex/desktop-app/linux.ts +1 -1
  84. package/src/codex/desktop-app/lock.ts +1 -1
  85. package/src/codex/desktop-app/types.ts +1 -1
  86. package/src/codex/desktop-app/windows.ts +1 -1
  87. package/src/codex/desktop-app-restart.ts +1 -1
  88. package/src/codex/main-account.ts +6 -7
  89. package/src/codex/native-profile-startup.ts +4 -1
  90. package/src/codex/prompt-journal.ts +20 -2
  91. package/src/codex/prompt-layers/encoding.ts +49 -0
  92. package/src/codex/prompt-layers/import-source.ts +30 -0
  93. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  94. package/src/codex/prompt-layers/toml-read.ts +52 -4
  95. package/src/codex/prompt-layers.ts +383 -20
  96. package/src/codex/prompt-lock-claim.ts +13 -3
  97. package/src/codex/prompt-lock-evidence.ts +3 -3
  98. package/src/codex/prompt-lock-owner.ts +14 -6
  99. package/src/codex/prompt-text-probe.ts +34 -5
  100. package/src/codex/quota.ts +13 -6
  101. package/src/codex/role-auto-assign.ts +2 -5
  102. package/src/codex/routing/active-account.ts +21 -0
  103. package/src/codex/routing.ts +1 -1
  104. package/src/codex/runtime.ts +196 -8
  105. package/src/codex/shim-overlay.ts +8 -4
  106. package/src/codex/shim.ts +1 -1
  107. package/src/combos/failover.ts +3 -0
  108. package/src/config/api-key-reconcile.ts +34 -0
  109. package/src/config/diagnostics.ts +16 -0
  110. package/src/config/feature-flags.ts +5 -0
  111. package/src/config/live-reconcile.ts +43 -29
  112. package/src/config/live-topology.ts +29 -0
  113. package/src/config/load-degrade.ts +2 -1
  114. package/src/config/paths.ts +3 -0
  115. package/src/config/salvage.ts +3 -1
  116. package/src/config/schema/config-schema.ts +2 -0
  117. package/src/generated/compatibility-version.json +373 -217
  118. package/src/grok/grpc-web.ts +23 -1
  119. package/src/grok/inject.ts +245 -221
  120. package/src/grok/reset-coupon-ledger.ts +293 -57
  121. package/src/grok/reset-coupons.ts +15 -6
  122. package/src/integrations/cursor-detect.ts +1 -1
  123. package/src/integrations/mutation-plan.ts +13 -7
  124. package/src/integrations/registry.ts +45 -11
  125. package/src/integrations/state.ts +19 -7
  126. package/src/integrations/target.ts +42 -5
  127. package/src/integrations/writer.ts +14 -6
  128. package/src/lib/app-owned-memory-stores.ts +4 -0
  129. package/src/lib/app-owned-memory.ts +11 -0
  130. package/src/lib/errors.ts +25 -0
  131. package/src/lib/json-byte-size.ts +31 -0
  132. package/src/lib/optional-shutdown-hooks.ts +35 -0
  133. package/src/lib/outbound-credential-registry.ts +210 -0
  134. package/src/lib/passive-read.ts +35 -0
  135. package/src/lib/redact.ts +2 -2
  136. package/src/lib/shadow-call.ts +44 -5
  137. package/src/link/codex-settings.ts +155 -0
  138. package/src/link/management-routes.ts +89 -0
  139. package/src/link/remote-probe.ts +38 -0
  140. package/src/link/ssh-argv.ts +26 -5
  141. package/src/link/ssh-runner.ts +58 -27
  142. package/src/link/status-projection.ts +22 -4
  143. package/src/link/supervisor.ts +144 -49
  144. package/src/link/tunnel-state.ts +11 -4
  145. package/src/oauth/anthropic-continuity.ts +4 -3
  146. package/src/oauth/anthropic-identity.ts +13 -2
  147. package/src/oauth/anthropic-model-quota.ts +13 -1
  148. package/src/oauth/anthropic-routing.ts +230 -23
  149. package/src/oauth/anthropic.ts +18 -4
  150. package/src/oauth/google-antigravity.ts +24 -7
  151. package/src/oauth/index.ts +25 -16
  152. package/src/oauth/kiro-account-identity.ts +106 -0
  153. package/src/oauth/kiro.ts +48 -5
  154. package/src/oauth/meta-muse-device.ts +1 -1
  155. package/src/oauth/plan.ts +29 -0
  156. package/src/oauth/store.ts +40 -3
  157. package/src/oauth/types.ts +3 -1
  158. package/src/plugins/loader.ts +11 -0
  159. package/src/providers/anthropic-billing-ownership.ts +74 -0
  160. package/src/providers/anthropic-billing-recovery.ts +31 -0
  161. package/src/providers/anthropic-billing-refusal.ts +55 -0
  162. package/src/providers/anthropic-fast.ts +1 -1
  163. package/src/providers/anthropic-reset-admission.ts +62 -0
  164. package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
  165. package/src/providers/api-key-resolve.ts +17 -2
  166. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  167. package/src/providers/devin-provider-merge-migration.ts +1 -1
  168. package/src/providers/github-copilot-auto.ts +169 -0
  169. package/src/providers/key-failover.ts +52 -5
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/model-rename-startup.ts +5 -3
  172. package/src/providers/muse-subscription-usage.ts +1 -1
  173. package/src/providers/quota/account-cache.ts +11 -0
  174. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  175. package/src/providers/quota/report-cache.ts +14 -0
  176. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  177. package/src/providers/quota.ts +4 -1
  178. package/src/providers/registry/entries-core.ts +20 -20
  179. package/src/providers/registry/entries-extended.ts +4 -1
  180. package/src/providers/registry/model-seeds.ts +20 -3
  181. package/src/providers/request-pacing.ts +9 -0
  182. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  183. package/src/providers/xai-fast-model.ts +1 -1
  184. package/src/responses/compaction.ts +14 -0
  185. package/src/responses/parser.ts +7 -1
  186. package/src/server/auth-cors.ts +42 -0
  187. package/src/server/configured-credentials.ts +194 -0
  188. package/src/server/inbound-body-admission.ts +24 -7
  189. package/src/server/index/link-listener.ts +30 -17
  190. package/src/server/index/link-management.ts +58 -0
  191. package/src/server/index/optional-listeners.ts +5 -1
  192. package/src/server/index/serve-options.ts +21 -2
  193. package/src/server/index.ts +1 -1
  194. package/src/server/lifecycle.ts +33 -5
  195. package/src/server/management/agent-settings-routes.ts +28 -10
  196. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  197. package/src/server/management/codex-agent-role-routes.ts +19 -6
  198. package/src/server/management/codex-prompt-routes.ts +136 -5
  199. package/src/server/management/codex-role-auto-assign.ts +1 -1
  200. package/src/server/management/grok-coupon-routes.ts +333 -57
  201. package/src/server/management/link-enrollment.ts +192 -0
  202. package/src/server/management/link-route-state.ts +185 -0
  203. package/src/server/management/link-routes.ts +92 -390
  204. package/src/server/management/model-rows.ts +3 -1
  205. package/src/server/management/provider-overwrite-carry.ts +10 -0
  206. package/src/server/management/provider-routes.ts +1 -0
  207. package/src/server/management/route-registry.ts +7 -5
  208. package/src/server/management/shared.ts +3 -0
  209. package/src/server/management-api.ts +3 -1
  210. package/src/server/messages-native.ts +68 -42
  211. package/src/server/relay-eager.ts +4 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-failure-attribution.ts +3 -0
  214. package/src/server/request-log-filter.ts +6 -1
  215. package/src/server/request-log-terminal-status.ts +243 -0
  216. package/src/server/request-log.ts +15 -118
  217. package/src/server/responses/adapter-continuation.ts +9 -1
  218. package/src/server/responses/adapter-dispatch.ts +293 -81
  219. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  220. package/src/server/responses/codex-ws-exchange.ts +147 -6
  221. package/src/server/responses/codex-ws-wire.ts +203 -0
  222. package/src/server/responses/collaboration.ts +36 -7
  223. package/src/server/responses/compact.ts +129 -50
  224. package/src/server/responses/context-overflow.ts +13 -1
  225. package/src/server/responses/core-codex-account.ts +105 -1
  226. package/src/server/responses/core-combo-failure.ts +14 -0
  227. package/src/server/responses/core-combo.ts +7 -2
  228. package/src/server/responses/core-errors.ts +29 -1
  229. package/src/server/responses/core-opaque-recovery.ts +5 -5
  230. package/src/server/responses/core-options.ts +2 -0
  231. package/src/server/responses/core.ts +6 -6
  232. package/src/server/responses/deferred-collab-surface.ts +158 -0
  233. package/src/server/responses/deferred-send-reporter.ts +25 -0
  234. package/src/server/responses/empty-completion-guard.ts +18 -2
  235. package/src/server/responses/fetch-helpers.ts +12 -1
  236. package/src/server/responses/jev-model-invoke.ts +1 -1
  237. package/src/server/responses/passthrough-delivery.ts +4 -1
  238. package/src/server/responses/passthrough-dispatch.ts +130 -64
  239. package/src/server/responses/passthrough-execution.ts +3 -3
  240. package/src/server/responses/request-prepare.ts +6 -4
  241. package/src/server/responses/request-transport.ts +131 -13
  242. package/src/server/responses/run-turn-execution.ts +13 -5
  243. package/src/server/responses/sidecar-execution.ts +16 -2
  244. package/src/server/responses/spend-storage-error.ts +33 -0
  245. package/src/server/responses/ws-upstream.ts +9 -1
  246. package/src/server/startup-action-control.ts +22 -2
  247. package/src/server/startup-health-cache.ts +4 -0
  248. package/src/server/system-env-shell.ts +7 -2
  249. package/src/server/system-env.ts +11 -1
  250. package/src/service/cli.ts +5 -1
  251. package/src/service/desktop-startup.ts +147 -34
  252. package/src/service/desktop-supervision.d.mts +27 -1
  253. package/src/service/desktop-supervision.mjs +111 -5
  254. package/src/service/health-sentinel-launch.ts +176 -0
  255. package/src/service/health-sentinel-main.ts +447 -0
  256. package/src/service/health-sentinel-policy.ts +241 -0
  257. package/src/service/health-sentinel-probe.ts +54 -0
  258. package/src/service/health-sentinel-windows.ts +87 -0
  259. package/src/service/linux-linger.ts +20 -0
  260. package/src/types/config.ts +5 -0
  261. package/src/types/request.ts +5 -0
  262. package/src/usage/expected-prices.ts +5 -1
  263. package/src/usage/log.ts +19 -1
  264. package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
  265. package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
  266. package/gui/dist/assets/index-D3FBCNiP.js +0 -94
@@ -1,9 +1,11 @@
1
1
  import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
2
2
  import {
3
+ isTransientUpstreamStatus,
3
4
  markResponseNonReplayable,
4
5
  UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
5
6
  UPSTREAM_NO_RESPONSE_CODE,
6
7
  } from "../../lib/upstream-retry";
8
+ import { isRateLimitOrQuotaFailureMessage, recognizedUpstreamError } from "../../lib/errors";
7
9
  import type { RequestFailureCause, RequestFailureStage } from "../../lib/request-failure-model";
8
10
  import { packageVersion } from "../../lib/package-version";
9
11
  // If the 101 never arrives (network black hole), give SSE a chance well before
@@ -107,8 +109,209 @@ export type CodexWsStageRecord = Omit<CodexWsFailureStage, "requestBytes"> & {
107
109
  ocxVersion: string;
108
110
  /** Bun runtime version the exchange gated on. */
109
111
  bunVersion: string;
112
+ /**
113
+ * Normalized type of the Responses terminal frame that ended a plain exchange, as the upstream
114
+ * sent it and before any tail this proxy synthesizes (#6740). It is what separates an upstream
115
+ * `error` frame from a genuine `response.failed` when both reach a row as the same synthetic 502.
116
+ * Null when no terminal frame arrived, and on native-control exchanges, which can end on an
117
+ * acknowledgement instead. Records written before it existed read as null.
118
+ */
119
+ terminalEvent: CodexWsTerminalEvent | null;
120
+ /** Wrapper status of a terminal `error` frame ({@link codexWsWrapperStatus}); null otherwise. */
121
+ terminalErrorStatus: number | null;
110
122
  };
111
123
 
124
+ /** The frames a plain exchange ends on, closed so a stage record can never carry free text. */
125
+ export const CODEX_WS_TERMINAL_EVENTS = [
126
+ "response.completed",
127
+ "response.failed",
128
+ "response.incomplete",
129
+ "error",
130
+ ] as const;
131
+ export type CodexWsTerminalEvent = typeof CODEX_WS_TERMINAL_EVENTS[number];
132
+
133
+ export function isCodexWsTerminalEvent(value: unknown): value is CodexWsTerminalEvent {
134
+ return typeof value === "string" && (CODEX_WS_TERMINAL_EVENTS as readonly string[]).includes(value);
135
+ }
136
+
137
+ /**
138
+ * The HTTP status a Codex WebSocket `error` frame states on its wrapper, or undefined.
139
+ *
140
+ * The backend wraps a failure as `{type:"error", status|status_code, error}` and the native client
141
+ * turns that status into an HTTP error of the same status
142
+ * (codex-rs/codex-api/src/endpoint/responses_websocket.rs map_wrapped_websocket_error_event). Only
143
+ * that contract is read: a top-level integer in 100..599 under exactly one of the two aliases, the
144
+ * same rule the pre-commit rejection projection applies.
145
+ */
146
+ export function codexWsWrapperStatus(payload: Record<string, unknown>): number | undefined {
147
+ if (payload.type !== "error") return undefined;
148
+ const hasStatus = Object.hasOwn(payload, "status");
149
+ if (hasStatus === Object.hasOwn(payload, "status_code")) return undefined;
150
+ const status = hasStatus ? payload.status : payload.status_code;
151
+ return typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599
152
+ ? status
153
+ : undefined;
154
+ }
155
+
156
+ function plainRecord(value: unknown): value is Record<string, unknown> {
157
+ return value !== null && typeof value === "object" && !Array.isArray(value);
158
+ }
159
+
160
+ /** An absent or empty `output` array; anything else may already be visible to the client. */
161
+ function emptyOutput(response: Record<string, unknown>): boolean {
162
+ const output = response.output;
163
+ return output === undefined || (Array.isArray(output) && output.length === 0);
164
+ }
165
+
166
+ /**
167
+ * Upper bound on how long an exchange may hold empty lifecycle frames while it waits to see
168
+ * whether the upstream refuses the turn before any output (#6969).
169
+ *
170
+ * Field rows put 97.5% of authoritative refusals within 5 s of the first Responses event and
171
+ * 98.2% within 10 s, with nothing gained after that.
172
+ */
173
+ export const CODEX_WS_PRE_OUTPUT_REFUSAL_HOLD_MAX_MS = 10_000;
174
+
175
+ /**
176
+ * The hold one send may use: at most `maxMs` from the first empty lifecycle frame, and never past
177
+ * `until`, an absolute time.
178
+ */
179
+ export interface CodexWsPreOutputRefusalHold {
180
+ maxMs: number;
181
+ until: number;
182
+ }
183
+
184
+ /**
185
+ * The hold for a send starting now. The caller's header deadline (`connectTimeoutMs`) starts no
186
+ * earlier than this, so ending every hold by half of it means a held turn always commits well before
187
+ * that deadline could abort it, however late the first lifecycle frame arrives.
188
+ */
189
+ export function codexWsPreOutputRefusalHold(connectMs: number, now = Date.now()): CodexWsPreOutputRefusalHold {
190
+ const half = Math.max(0, Math.floor(connectMs / 2));
191
+ return { maxMs: Math.min(CODEX_WS_PRE_OUTPUT_REFUSAL_HOLD_MAX_MS, half), until: now + half };
192
+ }
193
+
194
+ /**
195
+ * A lifecycle frame that commits nothing the client could see or act on (#6969): created,
196
+ * in-progress or queued, carrying a response object whose output is absent or empty. A snapshot
197
+ * with any output item, an unknown event, or a frame addressed to another stream is not one.
198
+ */
199
+ export function isEmptyCodexWsLifecycleEvent(payload: Record<string, unknown>): boolean {
200
+ if (payload.type !== "response.created" && payload.type !== "response.in_progress"
201
+ && payload.type !== "response.queued") return false;
202
+ if (payload.stream_id !== undefined) return false;
203
+ return plainRecord(payload.response) && emptyOutput(payload.response);
204
+ }
205
+
206
+ const QUOTA_REFUSAL_IDENTITIES: ReadonlySet<string> = new Set(["usage_limit_reached", "usage_not_included"]);
207
+
208
+ /** Quota and rate-limit identities keep their own routing: every `rate_limit*` and `*quota*` name. */
209
+ function quotaRefusalIdentity(value: string): boolean {
210
+ const name = value.toLowerCase();
211
+ return QUOTA_REFUSAL_IDENTITIES.has(name) || name.startsWith("rate_limit") || name.includes("quota");
212
+ }
213
+
214
+ /**
215
+ * The status of an authoritative upstream refusal that ended a turn before any output (#6969),
216
+ * or undefined.
217
+ *
218
+ * Only structured evidence counts: a bare `error` frame's single wrapper status (the alias the
219
+ * native client turns into an HTTP error of that status, see {@link codexWsWrapperStatus}), or,
220
+ * with no wrapper alias at all, the recognized overload class; and a `response.failed` with an
221
+ * empty output whose error is the recognized overload class. The status must be a transient
222
+ * gateway status. Message wording never authorizes anything here; it can only veto, so a 5xx whose
223
+ * error names quota or a rate limit keeps the routing it has today.
224
+ */
225
+ export function codexWsPreOutputRefusalStatus(payload: Record<string, unknown>): number | undefined {
226
+ if (payload.stream_id !== undefined) return undefined;
227
+ let error: unknown;
228
+ let status: number | undefined;
229
+ if (payload.type === "error") {
230
+ error = payload.error;
231
+ if (error !== undefined && !plainRecord(error)) return undefined;
232
+ const statedAlias = Object.hasOwn(payload, "status") || Object.hasOwn(payload, "status_code");
233
+ status = codexWsWrapperStatus(payload);
234
+ // A malformed or doubled alias is not evidence of anything.
235
+ if (statedAlias && status === undefined) return undefined;
236
+ if (status === undefined && plainRecord(error)) status = recognizedUpstreamError(error)?.httpStatus;
237
+ } else if (payload.type === "response.failed") {
238
+ const response = payload.response;
239
+ if (!plainRecord(response) || !emptyOutput(response)) return undefined;
240
+ error = response.error;
241
+ if (!plainRecord(error)) return undefined;
242
+ status = recognizedUpstreamError(error)?.httpStatus;
243
+ } else {
244
+ return undefined;
245
+ }
246
+ if (status === undefined || !isTransientUpstreamStatus(status)) return undefined;
247
+ if (plainRecord(error)) {
248
+ const { code, type, message } = error;
249
+ if ([code, type, message].some(value => value != null && typeof value !== "string")) return undefined;
250
+ if (typeof code === "string" && quotaRefusalIdentity(code)) return undefined;
251
+ if (typeof type === "string" && quotaRefusalIdentity(type)) return undefined;
252
+ if (typeof message === "string" && isRateLimitOrQuotaFailureMessage(message)) return undefined;
253
+ }
254
+ return status;
255
+ }
256
+
257
+ /**
258
+ * The response ids a frame names, for the exchange's replay-eligibility latch (#6969): the nested
259
+ * `response.id` and the top-level `response_id`. Null when a present id is not a string, which the
260
+ * latch treats as a conflict.
261
+ */
262
+ export function codexWsFrameResponseIds(payload: Record<string, unknown>): string[] | null {
263
+ const ids: string[] = [];
264
+ const nested = plainRecord(payload.response) ? payload.response.id : undefined;
265
+ for (const value of [nested, payload.response_id]) {
266
+ if (value === undefined) continue;
267
+ if (typeof value !== "string") return null;
268
+ if (!ids.includes(value)) ids.push(value);
269
+ }
270
+ return ids;
271
+ }
272
+
273
+ interface PreOutputRefusalRecord {
274
+ restore: (() => Response) | null;
275
+ outcomeRecorded: boolean;
276
+ }
277
+
278
+ const preOutputRefusals = new WeakMap<Response, PreOutputRefusalRecord>();
279
+
280
+ /**
281
+ * Register a pre-output refusal projection (#6969): a replayable gateway status standing in for a
282
+ * WebSocket exchange whose upstream refused the turn before any output. `restore` rebuilds the
283
+ * 200 event stream the client would have received without the projection.
284
+ */
285
+ export function markCodexWsPreOutputRefusal(projection: Response, restore: () => Response): void {
286
+ preOutputRefusals.set(projection, { restore, outcomeRecorded: false });
287
+ }
288
+
289
+ export function isCodexWsPreOutputRefusal(response: Response): boolean {
290
+ return preOutputRefusals.has(response);
291
+ }
292
+
293
+ /** Record that this projection's account outcome was written, so no later owner writes it again. */
294
+ export function noteCodexWsPreOutputRefusalOutcome(response: Response): void {
295
+ const record = preOutputRefusals.get(response);
296
+ if (record) record.outcomeRecorded = true;
297
+ }
298
+
299
+ export function codexWsPreOutputRefusalOutcomeRecorded(response: Response): boolean {
300
+ return preOutputRefusals.get(response)?.outcomeRecorded === true;
301
+ }
302
+
303
+ /**
304
+ * The response the client should receive in place of a projection no recovery replaced: the
305
+ * original event stream, byte for byte. Single use; any other response is returned unchanged.
306
+ */
307
+ export function restoreCodexWsPreOutputRefusal(response: Response): Response {
308
+ const record = preOutputRefusals.get(response);
309
+ const restore = record?.restore;
310
+ if (!record || !restore) return response;
311
+ record.restore = null;
312
+ return restore();
313
+ }
314
+
112
315
  const codexWsStageByResponse = new WeakMap<Response, CodexWsStageRecord>();
113
316
 
114
317
  export function markCodexWsStage(response: Response, record: CodexWsStageRecord): void {
@@ -9,6 +9,8 @@ import {
9
9
  import { parseRequest } from "../../responses/parser";
10
10
  import { externalTaskInputContent } from "../../responses/task-input";
11
11
  import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../codex/multi-agent-mode-policy";
12
+ import { analyzeCodeModeExec } from "./code-mode-collab-surface";
13
+ import { deferredV1Evidence } from "./deferred-collab-surface";
12
14
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
13
15
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
14
16
  import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
@@ -316,26 +318,53 @@ export function isV1CollabSurface(parsed: OcxParsedRequest): boolean {
316
318
 
317
319
 
318
320
  export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
321
+ const direct = directCollabSurface(parsed.context.tools);
322
+ // #6844: code-mode-only turns nest the collaboration tools inside `exec`, and tool-search turns
323
+ // defer them entirely. A rendered V1 listing or the deferred-namespace fragment can each infer V1;
324
+ // neither ever yields v2, overrides a different direct surface, or survives a rendered foreign
325
+ // collaboration claim.
326
+ const codeMode = analyzeCodeModeExec(parsed.context.tools);
327
+ if (codeMode.renderedV1 && codeMode.foreignCollabClaim) return null;
328
+ const inferredV1 = codeMode.renderedV1 || deferredV1Evidence(parsed, codeMode);
329
+ if (!inferredV1) return direct.surface;
330
+ if (codeMode.foreignCollabClaim) return null;
331
+ return !direct.sawNativeCollabTool || direct.surface === "v1" ? "v1" : null;
332
+ }
333
+
334
+ function directCollabSurface(tools: OcxParsedRequest["context"]["tools"]): {
335
+ surface: "v1" | "v2" | null;
336
+ sawNativeCollabTool: boolean;
337
+ } {
319
338
  let namespacedSpawn = false;
320
339
  let flatSpawn = false;
321
340
  let v1Only = false;
322
341
  let v2Only = false;
323
- for (const t of parsed.context.tools ?? []) {
342
+ // Collaboration-named direct tools outside an MCP namespace; an MCP server's own send_message or
343
+ // spawn_agent says nothing about the native surface, matching the nested check.
344
+ let sawNativeCollabTool = false;
345
+ for (const t of tools ?? []) {
346
+ const native = !t.namespace?.startsWith("mcp__");
324
347
  if (t.name === "spawn_agent") {
325
348
  if (t.namespace) namespacedSpawn = true;
326
349
  else flatSpawn = true;
350
+ if (native) sawNativeCollabTool = true;
327
351
  } else if (t.name === "send_input" || t.name === "resume_agent" || t.name === "close_agent") {
328
352
  v1Only = true;
353
+ if (native) sawNativeCollabTool = true;
329
354
  } else if (t.name === "send_message" || t.name === "followup_task" || t.name === "interrupt_agent" || t.name === "list_agents") {
330
355
  v2Only = true;
356
+ if (native) sawNativeCollabTool = true;
331
357
  }
332
358
  }
333
- if (!namespacedSpawn && !flatSpawn) return null; // no spawn_agent -> no collab surface
334
- if (namespacedSpawn && flatSpawn) return null; // contradictory spawn shapes
335
- if (v1Only && v2Only) return null; // contradictory companions
336
- if (v1Only) return "v1";
337
- if (v2Only) return "v2";
338
- return namespacedSpawn ? "v1" : "v2"; // companionless fallbacks (legacy defaults)
359
+ const surface = (): "v1" | "v2" | null => {
360
+ if (!namespacedSpawn && !flatSpawn) return null; // no spawn_agent -> no collab surface
361
+ if (namespacedSpawn && flatSpawn) return null; // contradictory spawn shapes
362
+ if (v1Only && v2Only) return null; // contradictory companions
363
+ if (v1Only) return "v1";
364
+ if (v2Only) return "v2";
365
+ return namespacedSpawn ? "v1" : "v2"; // companionless fallbacks (legacy defaults)
366
+ };
367
+ return { surface: surface(), sawNativeCollabTool };
339
368
  }
340
369
 
341
370
 
@@ -1,3 +1,4 @@
1
+ import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
1
2
  import { createPhysicalSendReporter } from "../../lib/request-execution-budget";
2
3
  import { createInferenceSendBudget } from "../inference/context";
3
4
  import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
@@ -199,6 +200,7 @@ import {
199
200
  usesCodexForwardPoolAuth,
200
201
  } from "./core";
201
202
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
203
+ import { shouldRetryCodexPoolAccountQuota, shouldRetryCodexPoolAccountTransient } from "./core-codex-account";
202
204
  import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
203
205
  import { linkRequestSessionLane, sessionLaneIdFromRequest } from "../request-log-conversation";
204
206
  import { recallComboForLane } from "./combo-session-recall";
@@ -1047,7 +1049,11 @@ export async function handleResponsesCompact(
1047
1049
  targetKey: compactTargetKey,
1048
1050
  });
1049
1051
  if (!decision.allowed) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
1050
- if (!decision.permit.use()) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
1052
+ if (!decision.permit.use()) {
1053
+ // Nothing was sent, so the reservation goes back rather than holding a send against the request.
1054
+ decision.permit.release();
1055
+ return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
1056
+ }
1051
1057
  return doFetch();
1052
1058
  };
1053
1059
  const sendCompactAttempt = (
@@ -1055,6 +1061,7 @@ export async function handleResponsesCompact(
1055
1061
  sendHeaders: Headers,
1056
1062
  recovery: "normal" | "single",
1057
1063
  sendAuthCtx: CodexAuthContext,
1064
+ onPhysicalDispatch?: () => void,
1058
1065
  ): Promise<Response> => {
1059
1066
  const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
1060
1067
  compactUrl,
@@ -1071,6 +1078,7 @@ export async function handleResponsesCompact(
1071
1078
  modelId: route.modelId,
1072
1079
  beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
1073
1080
  ? createCodexAuthDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
1081
+ ...(onPhysicalDispatch ? { onPhysicalDispatch } : {}),
1074
1082
  }),
1075
1083
  // Every credential-bearing forward send gets manual redirects, not only
1076
1084
  // pool sends: direct mode carries the caller's credential too (#914).
@@ -1099,11 +1107,11 @@ export async function handleResponsesCompact(
1099
1107
  let outcomeCtx = authCtx;
1100
1108
  const localDispatchRefusal = (error: unknown): Response | undefined => {
1101
1109
  const cause = unwrapUpstreamRetryEvidenceError(error);
1102
- const response = cause instanceof SendBudgetExhaustedError
1110
+ const response = spendLedgerStorageErrorResponse(error, logCtx) ?? (cause instanceof SendBudgetExhaustedError
1103
1111
  ? unboundPoolSpendRefusalResponse(logCtx) ?? formatErrorResponse(429, "request_send_budget_exhausted", cause.message)
1104
1112
  : mapCodexAuthContextErrorToResponse(cause, {
1105
1113
  now: Date.now(), accountSelector: route.codexAccountNamespace,
1106
- });
1114
+ }));
1107
1115
  if (response) {
1108
1116
  releaseUpstreamHostAdmission(compactHostAdmissionLease);
1109
1117
  compactHostAdmissionLease = null;
@@ -1216,6 +1224,53 @@ export async function handleResponsesCompact(
1216
1224
  }
1217
1225
  }
1218
1226
 
1227
+ // The settle for an alternate send that threw, shared by the quota and transient moves. By the
1228
+ // time it runs the request already names the alternate (outcomeCtx), so health goes to it.
1229
+ const failedAlternateCompactSend = (err: unknown): Response => {
1230
+ if (req.signal.aborted) {
1231
+ recordCompactPoolOutcome(outcomeCtx, 499);
1232
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1233
+ }
1234
+ const localRefusal = localDispatchRefusal(err);
1235
+ if (localRefusal) return localRefusal;
1236
+ const outcome = classifyTransportFailureKind(err);
1237
+ // Host-level evidence stands regardless of pool membership (#914 review).
1238
+ if (outcome === "connect_neutral") {
1239
+ if (compactHostCircuitEnabled) {
1240
+ recordUpstreamHostFailure(actualCompactHostKey, {
1241
+ code: transportErrorCode(err),
1242
+ threshold: config.upstreamHostCircuitThreshold,
1243
+ lease: compactHostAdmissionLease,
1244
+ });
1245
+ } else {
1246
+ recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
1247
+ }
1248
+ } else {
1249
+ releaseUpstreamHostAdmission(compactHostAdmissionLease);
1250
+ }
1251
+ compactHostAdmissionLease = null;
1252
+ recordCompactPoolOutcome(outcomeCtx, outcome);
1253
+ return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
1254
+ };
1255
+ const scrubCompactStateForAlternate = (alternateAuthCtx: CodexAuthContext): void => {
1256
+ const binding = conversationStateBindingFromAuth(
1257
+ alternateAuthCtx,
1258
+ (authCtx.kind === "pool" || authCtx.kind === "main-pool")
1259
+ ? authCtx.affinityKey
1260
+ : codexPoolAffinityKey(req.headers),
1261
+ );
1262
+ if (!binding) return;
1263
+ for (const body of [raw, compactBody]) {
1264
+ applyAccountChangeConversationStateScrub({
1265
+ body,
1266
+ bindingKey: binding.bindingKey,
1267
+ servingAccountId: binding.accountId,
1268
+ priorAccountId: authCtx.accountId,
1269
+ logCtx,
1270
+ });
1271
+ }
1272
+ };
1273
+
1219
1274
  // Bounded same-request alternate: the regular /v1/responses path already does this
1220
1275
  // (core.ts:319-423) and recognizes exactly 429/402. Without it a pool rejection
1221
1276
  // surfaces to the client, which retries the compact task OUTSIDE the logical request
@@ -1316,57 +1371,81 @@ export async function handleResponsesCompact(
1316
1371
  await upstream.body?.cancel().catch(() => undefined);
1317
1372
  outcomeCtx = alternate.authCtx;
1318
1373
  logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
1319
- {
1320
- const binding = conversationStateBindingFromAuth(
1321
- alternate.authCtx,
1322
- (authCtx.kind === "pool" || authCtx.kind === "main-pool")
1323
- ? authCtx.affinityKey
1324
- : codexPoolAffinityKey(req.headers),
1325
- );
1326
- if (binding) {
1327
- applyAccountChangeConversationStateScrub({
1328
- body: raw,
1329
- bindingKey: binding.bindingKey,
1330
- servingAccountId: binding.accountId,
1331
- priorAccountId: authCtx.accountId,
1332
- logCtx,
1333
- });
1334
- applyAccountChangeConversationStateScrub({
1335
- body: compactBody,
1336
- bindingKey: binding.bindingKey,
1337
- servingAccountId: binding.accountId,
1338
- priorAccountId: authCtx.accountId,
1339
- logCtx,
1340
- });
1341
- }
1342
- }
1374
+ scrubCompactStateForAlternate(alternate.authCtx);
1343
1375
  try {
1344
1376
  upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx);
1345
1377
  } catch (err) {
1346
- if (req.signal.aborted) {
1347
- recordCompactPoolOutcome(outcomeCtx, 499);
1348
- return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1349
- }
1350
- const localRefusal = localDispatchRefusal(err);
1351
- if (localRefusal) return localRefusal;
1352
- const outcome = classifyTransportFailureKind(err);
1353
- // Host-level evidence stands regardless of pool membership (#914 review).
1354
- if (outcome === "connect_neutral") {
1355
- if (compactHostCircuitEnabled) {
1356
- recordUpstreamHostFailure(actualCompactHostKey, {
1357
- code: transportErrorCode(err),
1358
- threshold: config.upstreamHostCircuitThreshold,
1359
- lease: compactHostAdmissionLease,
1360
- });
1361
- } else {
1362
- recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
1363
- }
1364
- } else {
1365
- releaseUpstreamHostAdmission(compactHostAdmissionLease);
1378
+ return failedAlternateCompactSend(err);
1379
+ }
1380
+ }
1381
+ }
1382
+ // A transient 5xx that survived the same-account ladder above (#6969). The regular path moves
1383
+ // it to another Pool account; compact used to move only on 429/402, so the client retried the
1384
+ // compact task outside this request while another account sat idle. One bounded alternate send,
1385
+ // the same guards as the quota move, and a 5xx whose body confirms quota keeps today's path.
1386
+ if (
1387
+ shouldRetryCodexPoolAccountTransient(upstream)
1388
+ && !storedPool401ReplayAttempted
1389
+ && usesCodexForwardPoolAuth(authCtx, route.provider)
1390
+ && !authCtx.fixedAccount
1391
+ && route.codexAccountMode
1392
+ && outcomeCtx === authCtx
1393
+ && !req.signal.aborted
1394
+ && !conversationCarriesUploadedFiles(raw)
1395
+ && !await shouldRetryCodexPoolAccountQuota(upstream, req.signal)
1396
+ ) {
1397
+ const firstStatus = upstream.status;
1398
+ const firstRetryAfter = upstream.headers.get("retry-after");
1399
+ const firstResetAt = [
1400
+ upstream.headers.get("x-codex-primary-reset-at"),
1401
+ upstream.headers.get("x-codex-secondary-reset-at"),
1402
+ upstream.headers.get("x-codex-tertiary-reset-at"),
1403
+ ].filter(Boolean);
1404
+ const alternate = await resolveAlternateCompactContext({
1405
+ req,
1406
+ admission,
1407
+ config,
1408
+ route,
1409
+ selectedModelId,
1410
+ excludeAccountId: authCtx.accountId,
1411
+ turnAdmissionLease,
1412
+ });
1413
+ if (alternate && req.signal.aborted) {
1414
+ releaseCodexAuthContextProbeLease(alternate.authCtx);
1415
+ recordCompactPoolOutcome(outcomeCtx, 499);
1416
+ void upstream.body?.cancel(req.signal.reason).catch(() => undefined);
1417
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1418
+ }
1419
+ if (alternate) {
1420
+ const firstLabel = logCtx.accountLogLabel;
1421
+ // B's identity before B's admission, so every budget and spend check runs against B. A's
1422
+ // response stays intact until B is actually sent.
1423
+ logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
1424
+ scrubCompactStateForAlternate(alternate.authCtx);
1425
+ const recordFirstMove = () => {
1426
+ recordCompactPoolOutcome(authCtx, firstStatus, { retryAfter: firstRetryAfter, resetAt: firstResetAt });
1427
+ void upstream.body?.cancel().catch(() => undefined);
1428
+ outcomeCtx = alternate.authCtx;
1429
+ };
1430
+ let alternateResponse: Response | undefined;
1431
+ // Whether B's send actually left this process. Every local refusal before that point -- a
1432
+ // spent budget, a dispatch guard such as disabled credits, a pacing refusal -- keeps A.
1433
+ let alternateDispatched = false;
1434
+ try {
1435
+ alternateResponse = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx,
1436
+ () => { alternateDispatched = true; });
1437
+ } catch (err) {
1438
+ if (alternateDispatched) {
1439
+ recordFirstMove();
1440
+ return failedAlternateCompactSend(err);
1366
1441
  }
1367
- compactHostAdmissionLease = null;
1368
- recordCompactPoolOutcome(outcomeCtx, outcome);
1369
- return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
1442
+ // Refused before anything left: A's answer and identity stand, recorded once below.
1443
+ logCtx.accountLogLabel = firstLabel;
1444
+ releaseCodexAuthContextProbeLease(alternate.authCtx);
1445
+ }
1446
+ if (alternateResponse) {
1447
+ recordFirstMove();
1448
+ upstream = alternateResponse;
1370
1449
  }
1371
1450
  }
1372
1451
  }
@@ -1,7 +1,19 @@
1
- import { bridgeToResponsesSSE } from "../../bridge";
1
+ import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
2
+ import { OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE } from "../../adapters/ollama-native";
2
3
  import type { TranslatorBudget } from "../../lib/translator-budget";
3
4
  import type { AdapterEvent } from "../../types";
4
5
 
6
+ /**
7
+ * The Ollama late-attribution refusal as a client response: the existing translator-budget
8
+ * wire shape (413 request_too_large) with only the proxy-owned constant as its message.
9
+ * It is deliberately not a context overflow: compacting on the same route cannot shrink it.
10
+ */
11
+ export function lateAttributionLimitResponse(): Response {
12
+ return formatErrorResponse(413, "request_too_large", OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE, {
13
+ code: "translation_buffer_limit",
14
+ });
15
+ }
16
+
5
17
  export const PROVIDER_INPUT_TOO_LARGE_MESSAGE =
6
18
  "The provider rejected this turn because its input exceeds the provider size or context limit. Reduce the current input or compact the conversation before retrying.";
7
19