@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.
- package/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +19 -0
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export.ts +46 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +311 -171
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +24 -15
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +14 -14
- package/src/providers/registry/entries-extended.ts +0 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +13 -0
- package/src/server/lifecycle.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +5 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/responses/adapter-dispatch.ts +232 -79
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -4
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +6 -8
- package/src/server/responses/sidecar-execution.ts +11 -2
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/usage/expected-prices.ts +4 -0
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- 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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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())
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
|