@bitkyc08/opencodex 2.83.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 (216) 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-DiJO6sYY.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/ollama-native.ts +29 -1
  10. package/src/adapters/openai-chat/messages.ts +8 -4
  11. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  12. package/src/adapters/openai-responses/passthrough.ts +3 -0
  13. package/src/adapters/openai-responses/request-strips.ts +10 -2
  14. package/src/adapters/qoder/adapter.ts +16 -14
  15. package/src/bridge/errors.ts +19 -0
  16. package/src/bridge.ts +1 -1
  17. package/src/claude/context-windows.ts +54 -1
  18. package/src/claude/intercept/connect-proxy.ts +10 -3
  19. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  20. package/src/claude/intercept/picker-listener.ts +49 -1
  21. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  22. package/src/claude/intercept/settings.ts +101 -9
  23. package/src/cli/account-api.ts +65 -0
  24. package/src/cli/account-auth.ts +26 -2
  25. package/src/cli/account.ts +41 -5
  26. package/src/cli/capabilities-access-remote.ts +8 -0
  27. package/src/cli/capabilities-base.ts +13 -1
  28. package/src/cli/catalog-command-result.ts +2 -1
  29. package/src/cli/claude.ts +15 -6
  30. package/src/cli/config-command.ts +8 -2
  31. package/src/cli/debug.ts +8 -0
  32. package/src/cli/dispatch.ts +16 -3
  33. package/src/cli/export-command.ts +14 -1
  34. package/src/cli/hub-disable.ts +99 -0
  35. package/src/cli/hub.ts +4 -2
  36. package/src/cli/index.ts +13 -4
  37. package/src/cli/link-child.ts +104 -0
  38. package/src/cli/link-enrollment.ts +96 -0
  39. package/src/cli/link.ts +39 -24
  40. package/src/cli/observe-stream.ts +6 -1
  41. package/src/cli/observe.ts +10 -2
  42. package/src/cli/provider.ts +12 -0
  43. package/src/cli/registry.ts +5 -4
  44. package/src/cli/runtime-api.ts +27 -2
  45. package/src/cli/status.ts +104 -20
  46. package/src/client/connect.ts +39 -4
  47. package/src/client/hub-client.ts +14 -7
  48. package/src/client/hub-state.ts +25 -14
  49. package/src/client/link-home.ts +248 -0
  50. package/src/client/link-management-relay.ts +61 -0
  51. package/src/client/link-relay.ts +6 -1
  52. package/src/client/link-status.ts +14 -0
  53. package/src/client/machine-api.ts +14 -7
  54. package/src/client/machine-listener.ts +29 -4
  55. package/src/clients/config-export.ts +46 -16
  56. package/src/clients/omo-role-models.ts +6 -3
  57. package/src/codex/agent-role-models.ts +26 -8
  58. package/src/codex/auth-api/account-list.ts +17 -2
  59. package/src/codex/auth-api/account-selection.ts +17 -7
  60. package/src/codex/auth-api/routes.ts +5 -1
  61. package/src/codex/autostart-health.ts +3 -1
  62. package/src/codex/catalog/bundled.ts +5 -1
  63. package/src/codex/catalog/effort.ts +2 -2
  64. package/src/codex/catalog/metadata.ts +4 -2
  65. package/src/codex/catalog/native-models.ts +16 -2
  66. package/src/codex/native-profile-startup.ts +4 -1
  67. package/src/codex/prompt-journal.ts +20 -2
  68. package/src/codex/prompt-layers/encoding.ts +49 -0
  69. package/src/codex/prompt-layers/import-source.ts +30 -0
  70. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  71. package/src/codex/prompt-layers/toml-read.ts +52 -4
  72. package/src/codex/prompt-layers.ts +383 -20
  73. package/src/codex/prompt-lock-claim.ts +13 -3
  74. package/src/codex/prompt-lock-evidence.ts +3 -3
  75. package/src/codex/prompt-lock-owner.ts +14 -6
  76. package/src/codex/prompt-text-probe.ts +34 -5
  77. package/src/codex/quota.ts +13 -6
  78. package/src/codex/role-auto-assign.ts +2 -5
  79. package/src/codex/routing/active-account.ts +21 -0
  80. package/src/codex/routing.ts +1 -1
  81. package/src/codex/runtime.ts +196 -8
  82. package/src/codex/shim-overlay.ts +8 -4
  83. package/src/codex/shim.ts +1 -1
  84. package/src/combos/failover.ts +3 -0
  85. package/src/config/api-key-reconcile.ts +34 -0
  86. package/src/config/diagnostics.ts +16 -0
  87. package/src/config/feature-flags.ts +5 -0
  88. package/src/config/live-reconcile.ts +43 -29
  89. package/src/config/live-topology.ts +29 -0
  90. package/src/config/load-degrade.ts +2 -1
  91. package/src/config/paths.ts +3 -0
  92. package/src/config/salvage.ts +3 -1
  93. package/src/config/schema/config-schema.ts +2 -0
  94. package/src/generated/compatibility-version.json +311 -171
  95. package/src/grok/grpc-web.ts +23 -1
  96. package/src/grok/reset-coupon-ledger.ts +75 -5
  97. package/src/grok/reset-coupons.ts +4 -2
  98. package/src/integrations/mutation-plan.ts +13 -7
  99. package/src/integrations/registry.ts +45 -11
  100. package/src/integrations/state.ts +19 -7
  101. package/src/integrations/target.ts +42 -5
  102. package/src/integrations/writer.ts +14 -6
  103. package/src/lib/app-owned-memory-stores.ts +4 -0
  104. package/src/lib/app-owned-memory.ts +11 -0
  105. package/src/lib/json-byte-size.ts +31 -0
  106. package/src/lib/optional-shutdown-hooks.ts +35 -0
  107. package/src/lib/outbound-credential-registry.ts +210 -0
  108. package/src/lib/passive-read.ts +35 -0
  109. package/src/lib/shadow-call.ts +44 -5
  110. package/src/link/codex-settings.ts +155 -0
  111. package/src/link/management-routes.ts +89 -0
  112. package/src/link/remote-probe.ts +38 -0
  113. package/src/link/ssh-argv.ts +26 -5
  114. package/src/link/ssh-runner.ts +58 -27
  115. package/src/link/status-projection.ts +22 -4
  116. package/src/link/supervisor.ts +144 -49
  117. package/src/link/tunnel-state.ts +11 -4
  118. package/src/oauth/anthropic-continuity.ts +4 -3
  119. package/src/oauth/anthropic-identity.ts +13 -2
  120. package/src/oauth/anthropic-model-quota.ts +13 -1
  121. package/src/oauth/anthropic-routing.ts +230 -23
  122. package/src/oauth/anthropic.ts +18 -4
  123. package/src/oauth/google-antigravity.ts +24 -7
  124. package/src/oauth/index.ts +24 -15
  125. package/src/oauth/kiro-account-identity.ts +106 -0
  126. package/src/oauth/kiro.ts +48 -5
  127. package/src/oauth/plan.ts +29 -0
  128. package/src/oauth/store.ts +40 -3
  129. package/src/oauth/types.ts +2 -0
  130. package/src/plugins/loader.ts +11 -0
  131. package/src/providers/anthropic-billing-ownership.ts +74 -0
  132. package/src/providers/anthropic-billing-recovery.ts +31 -0
  133. package/src/providers/anthropic-billing-refusal.ts +55 -0
  134. package/src/providers/anthropic-reset-admission.ts +62 -0
  135. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  136. package/src/providers/api-key-resolve.ts +17 -2
  137. package/src/providers/key-failover.ts +52 -5
  138. package/src/providers/model-rename-startup.ts +5 -3
  139. package/src/providers/quota/account-cache.ts +11 -0
  140. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  141. package/src/providers/quota/report-cache.ts +14 -0
  142. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  143. package/src/providers/quota.ts +4 -1
  144. package/src/providers/registry/entries-core.ts +14 -14
  145. package/src/providers/registry/entries-extended.ts +0 -1
  146. package/src/providers/registry/model-seeds.ts +17 -0
  147. package/src/providers/request-pacing.ts +9 -0
  148. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  149. package/src/responses/compaction.ts +14 -0
  150. package/src/responses/parser.ts +7 -1
  151. package/src/server/auth-cors.ts +42 -0
  152. package/src/server/configured-credentials.ts +194 -0
  153. package/src/server/index/link-management.ts +58 -0
  154. package/src/server/index/optional-listeners.ts +5 -1
  155. package/src/server/index/serve-options.ts +13 -0
  156. package/src/server/lifecycle.ts +2 -1
  157. package/src/server/management/agent-settings-routes.ts +28 -10
  158. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  159. package/src/server/management/codex-agent-role-routes.ts +19 -6
  160. package/src/server/management/codex-prompt-routes.ts +136 -5
  161. package/src/server/management/codex-role-auto-assign.ts +1 -1
  162. package/src/server/management/grok-coupon-routes.ts +78 -27
  163. package/src/server/management/link-enrollment.ts +192 -0
  164. package/src/server/management/link-route-state.ts +185 -0
  165. package/src/server/management/link-routes.ts +82 -402
  166. package/src/server/management/model-rows.ts +3 -1
  167. package/src/server/management/provider-overwrite-carry.ts +10 -0
  168. package/src/server/management/provider-routes.ts +1 -0
  169. package/src/server/management/route-registry.ts +5 -3
  170. package/src/server/management/shared.ts +3 -0
  171. package/src/server/management-api.ts +3 -1
  172. package/src/server/messages-native.ts +68 -42
  173. package/src/server/relay-eager.ts +2 -0
  174. package/src/server/request-log-filter.ts +6 -1
  175. package/src/server/request-log-terminal-status.ts +63 -6
  176. package/src/server/responses/adapter-dispatch.ts +232 -79
  177. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  178. package/src/server/responses/codex-ws-exchange.ts +147 -6
  179. package/src/server/responses/codex-ws-wire.ts +203 -0
  180. package/src/server/responses/collaboration.ts +36 -7
  181. package/src/server/responses/compact.ts +126 -48
  182. package/src/server/responses/context-overflow.ts +13 -1
  183. package/src/server/responses/core-codex-account.ts +105 -1
  184. package/src/server/responses/core-combo-failure.ts +14 -0
  185. package/src/server/responses/core-combo.ts +7 -2
  186. package/src/server/responses/core-errors.ts +29 -1
  187. package/src/server/responses/core-options.ts +2 -0
  188. package/src/server/responses/deferred-collab-surface.ts +158 -0
  189. package/src/server/responses/deferred-send-reporter.ts +25 -0
  190. package/src/server/responses/fetch-helpers.ts +12 -1
  191. package/src/server/responses/passthrough-delivery.ts +4 -1
  192. package/src/server/responses/passthrough-dispatch.ts +15 -4
  193. package/src/server/responses/request-prepare.ts +6 -4
  194. package/src/server/responses/request-transport.ts +6 -8
  195. package/src/server/responses/sidecar-execution.ts +11 -2
  196. package/src/server/responses/ws-upstream.ts +9 -1
  197. package/src/server/startup-action-control.ts +22 -2
  198. package/src/server/startup-health-cache.ts +4 -0
  199. package/src/server/system-env-shell.ts +7 -2
  200. package/src/server/system-env.ts +11 -1
  201. package/src/service/cli.ts +5 -1
  202. package/src/service/desktop-startup.ts +147 -34
  203. package/src/service/desktop-supervision.d.mts +27 -1
  204. package/src/service/desktop-supervision.mjs +111 -5
  205. package/src/service/health-sentinel-launch.ts +176 -0
  206. package/src/service/health-sentinel-main.ts +447 -0
  207. package/src/service/health-sentinel-policy.ts +241 -0
  208. package/src/service/health-sentinel-probe.ts +54 -0
  209. package/src/service/health-sentinel-windows.ts +87 -0
  210. package/src/service/linux-linger.ts +20 -0
  211. package/src/types/config.ts +5 -0
  212. package/src/usage/expected-prices.ts +4 -0
  213. package/src/usage/log.ts +19 -1
  214. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  215. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  216. package/gui/dist/assets/index-BHRZdJuv.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
 
@@ -200,6 +200,7 @@ import {
200
200
  usesCodexForwardPoolAuth,
201
201
  } from "./core";
202
202
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
203
+ import { shouldRetryCodexPoolAccountQuota, shouldRetryCodexPoolAccountTransient } from "./core-codex-account";
203
204
  import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
204
205
  import { linkRequestSessionLane, sessionLaneIdFromRequest } from "../request-log-conversation";
205
206
  import { recallComboForLane } from "./combo-session-recall";
@@ -1048,7 +1049,11 @@ export async function handleResponsesCompact(
1048
1049
  targetKey: compactTargetKey,
1049
1050
  });
1050
1051
  if (!decision.allowed) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
1051
- 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
+ }
1052
1057
  return doFetch();
1053
1058
  };
1054
1059
  const sendCompactAttempt = (
@@ -1056,6 +1061,7 @@ export async function handleResponsesCompact(
1056
1061
  sendHeaders: Headers,
1057
1062
  recovery: "normal" | "single",
1058
1063
  sendAuthCtx: CodexAuthContext,
1064
+ onPhysicalDispatch?: () => void,
1059
1065
  ): Promise<Response> => {
1060
1066
  const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
1061
1067
  compactUrl,
@@ -1072,6 +1078,7 @@ export async function handleResponsesCompact(
1072
1078
  modelId: route.modelId,
1073
1079
  beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
1074
1080
  ? createCodexAuthDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
1081
+ ...(onPhysicalDispatch ? { onPhysicalDispatch } : {}),
1075
1082
  }),
1076
1083
  // Every credential-bearing forward send gets manual redirects, not only
1077
1084
  // pool sends: direct mode carries the caller's credential too (#914).
@@ -1217,6 +1224,53 @@ export async function handleResponsesCompact(
1217
1224
  }
1218
1225
  }
1219
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
+
1220
1274
  // Bounded same-request alternate: the regular /v1/responses path already does this
1221
1275
  // (core.ts:319-423) and recognizes exactly 429/402. Without it a pool rejection
1222
1276
  // surfaces to the client, which retries the compact task OUTSIDE the logical request
@@ -1317,57 +1371,81 @@ export async function handleResponsesCompact(
1317
1371
  await upstream.body?.cancel().catch(() => undefined);
1318
1372
  outcomeCtx = alternate.authCtx;
1319
1373
  logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
1320
- {
1321
- const binding = conversationStateBindingFromAuth(
1322
- alternate.authCtx,
1323
- (authCtx.kind === "pool" || authCtx.kind === "main-pool")
1324
- ? authCtx.affinityKey
1325
- : codexPoolAffinityKey(req.headers),
1326
- );
1327
- if (binding) {
1328
- applyAccountChangeConversationStateScrub({
1329
- body: raw,
1330
- bindingKey: binding.bindingKey,
1331
- servingAccountId: binding.accountId,
1332
- priorAccountId: authCtx.accountId,
1333
- logCtx,
1334
- });
1335
- applyAccountChangeConversationStateScrub({
1336
- body: compactBody,
1337
- bindingKey: binding.bindingKey,
1338
- servingAccountId: binding.accountId,
1339
- priorAccountId: authCtx.accountId,
1340
- logCtx,
1341
- });
1342
- }
1343
- }
1374
+ scrubCompactStateForAlternate(alternate.authCtx);
1344
1375
  try {
1345
1376
  upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx);
1346
1377
  } catch (err) {
1347
- if (req.signal.aborted) {
1348
- recordCompactPoolOutcome(outcomeCtx, 499);
1349
- return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1350
- }
1351
- const localRefusal = localDispatchRefusal(err);
1352
- if (localRefusal) return localRefusal;
1353
- const outcome = classifyTransportFailureKind(err);
1354
- // Host-level evidence stands regardless of pool membership (#914 review).
1355
- if (outcome === "connect_neutral") {
1356
- if (compactHostCircuitEnabled) {
1357
- recordUpstreamHostFailure(actualCompactHostKey, {
1358
- code: transportErrorCode(err),
1359
- threshold: config.upstreamHostCircuitThreshold,
1360
- lease: compactHostAdmissionLease,
1361
- });
1362
- } else {
1363
- recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
1364
- }
1365
- } else {
1366
- 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);
1367
1441
  }
1368
- compactHostAdmissionLease = null;
1369
- recordCompactPoolOutcome(outcomeCtx, outcome);
1370
- 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;
1371
1449
  }
1372
1450
  }
1373
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