@bitkyc08/opencodex 2.57.0 → 2.58.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/{index-Cz7CLdif.js → index-BbrHOIY0.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-responses/passthrough.ts +30 -4
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/claude/outbound.ts +14 -4
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +44 -2
- package/src/cli/system-command.ts +70 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/codex/auth-context.ts +42 -8
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +3 -7
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/config/atomic-write.ts +83 -8
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +132 -68
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/providers/derive.ts +6 -0
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +50 -28
- package/src/providers/registry/model-seeds.ts +67 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +1 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +11 -0
- package/src/server/responses/passthrough-dispatch.ts +33 -1
- package/src/server/responses/request-prepare.ts +41 -0
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +13 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mergeSteeringContinuation } from "./native-steering-settings";
|
|
2
|
+
import { markNativeControlResponse } from "./native-response-control";
|
|
3
|
+
import type { NativeResponseControl } from "./native-response-control";
|
|
1
4
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
2
5
|
import { isSafeResponseHeader } from "../safe-response-headers";
|
|
3
6
|
import { CodexWsMetadata, type CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
@@ -6,10 +9,12 @@ import { CodexWsCorrelation } from "./codex-ws-correlation";
|
|
|
6
9
|
import type { CodexWsSession } from "./codex-ws-session";
|
|
7
10
|
import { UPGRADE_DEADLINE_MS, CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
|
|
8
11
|
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
|
|
9
|
-
codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
|
|
12
|
+
codexWsCreateFrameExceedsLimit, codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
|
|
10
13
|
type CodexWsFailureStage, type CodexWsStageRecord } from "./codex-ws-wire";
|
|
11
14
|
|
|
12
15
|
interface ExchangeOptions {
|
|
16
|
+
nativeControl?: NativeResponseControl;
|
|
17
|
+
beforeContinuation?: () => Promise<void>;
|
|
13
18
|
session: CodexWsSession;
|
|
14
19
|
url: string;
|
|
15
20
|
init: RequestInit;
|
|
@@ -86,7 +91,7 @@ function wrappedRejectionResponse(payload: Record<string, unknown>, prelude: Hea
|
|
|
86
91
|
|
|
87
92
|
/** The sole SSE exchange state machine for both one-shot and retained sockets. */
|
|
88
93
|
export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
89
|
-
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion } = options;
|
|
94
|
+
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion, nativeControl, beforeContinuation } = options;
|
|
90
95
|
const { frameText, headers } = prepared;
|
|
91
96
|
const signal = init.signal ?? undefined;
|
|
92
97
|
return new Promise<Response>((resolve, reject) => {
|
|
@@ -116,6 +121,8 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
116
121
|
const metadata = url === CODEX_RESPONSES_HTTP_URL ? new CodexWsMetadata(onQuota) : null;
|
|
117
122
|
const correlation = session.retainable ? new CodexWsCorrelation(session.reused, id => session.hasCompleted(id)) : null;
|
|
118
123
|
let detachOwner = () => {};
|
|
124
|
+
let detachSteering = () => {};
|
|
125
|
+
let continuationBase: Record<string, unknown> | undefined;
|
|
119
126
|
// Liveness while waiting for the first response event (metadata path only): the
|
|
120
127
|
// silence timer is re-armed by every inbound frame or pong; the pinger runs on a fixed
|
|
121
128
|
// interval so a peer that answers pings can never trip the silence bound while alive.
|
|
@@ -142,6 +149,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
142
149
|
metadata?.finish();
|
|
143
150
|
correlation?.finish();
|
|
144
151
|
detachOwner();
|
|
152
|
+
detachSteering();
|
|
145
153
|
ws.removeEventListener("open", onOpen);
|
|
146
154
|
ws.removeEventListener("message", onMessage);
|
|
147
155
|
ws.removeEventListener("close", onClose);
|
|
@@ -198,6 +206,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
198
206
|
const response = new Response(stream, { status: 200, headers: responseHeaders });
|
|
199
207
|
metadata?.commit();
|
|
200
208
|
markCodexWsResponse(response, Boolean(metadata && onQuota));
|
|
209
|
+
if (nativeControl) markNativeControlResponse(response);
|
|
201
210
|
markCodexWsStage(response, stageRecord(null));
|
|
202
211
|
committedResponse = response;
|
|
203
212
|
resolve(response);
|
|
@@ -329,6 +338,54 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
329
338
|
}
|
|
330
339
|
if (terminal || settledPreOpen || signal?.aborted) return;
|
|
331
340
|
sent = true;
|
|
341
|
+
try {
|
|
342
|
+
if (nativeControl) {
|
|
343
|
+
// Parsed once: the base body is immutable for this exchange, and a
|
|
344
|
+
// full-replay frame runs to megabytes. It seeds continuationBase on
|
|
345
|
+
// the first create frame.
|
|
346
|
+
let base: Record<string, unknown> | undefined;
|
|
347
|
+
detachSteering = nativeControl.attach(frame => {
|
|
348
|
+
const sendControl = () => {
|
|
349
|
+
if (terminal || signal?.aborted || session.closed || ws.readyState !== WebSocket.OPEN) {
|
|
350
|
+
throw new Error("Native steering connection is no longer available");
|
|
351
|
+
}
|
|
352
|
+
beforeDispatch?.(new Headers(headers));
|
|
353
|
+
let outgoing = frame;
|
|
354
|
+
if (frame.type === "response.create") {
|
|
355
|
+
// Generation overrides have passed route policy; identity/tools remain pinned.
|
|
356
|
+
// Keep the last explicit wire settings for later explicit and automatic successors.
|
|
357
|
+
base ??= JSON.parse(frameText) as Record<string, unknown>;
|
|
358
|
+
continuationBase ??= base;
|
|
359
|
+
outgoing = nativeControl.kind === "steering"
|
|
360
|
+
? mergeSteeringContinuation(continuationBase, frame)
|
|
361
|
+
: { ...continuationBase, input: frame.input, previous_response_id: frame.previous_response_id };
|
|
362
|
+
}
|
|
363
|
+
const text = JSON.stringify(outgoing);
|
|
364
|
+
if (codexWsCreateFrameExceedsLimit(text)) {
|
|
365
|
+
throw new Error("Native steering frame exceeds the transport byte limit");
|
|
366
|
+
}
|
|
367
|
+
if (frame.type === "response.create") continuationBase = outgoing;
|
|
368
|
+
try { ws.send(text); } catch {
|
|
369
|
+
// A send failure has unknown delivery. Never replay or fall back.
|
|
370
|
+
failStream("Native steering send failed; delivery is unknown");
|
|
371
|
+
throw new Error("Native steering send failed; delivery is unknown");
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
if (frame.type === "response.create" && beforeContinuation) {
|
|
375
|
+
// Explicit tool-result continuations are physical request starts;
|
|
376
|
+
// they keep provider pacing and revalidate auth AFTER the wait.
|
|
377
|
+
void beforeContinuation().then(sendControl).catch(() => failStream("Native steering continuation could not be dispatched; do not automatically replay queued input"));
|
|
378
|
+
} else sendControl();
|
|
379
|
+
}, error => failStream(error));
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
// An attach failure is an ownership conflict, not a failed send: no frame
|
|
383
|
+
// left the process, but the channel can never bind, so resolving the HTTP
|
|
384
|
+
// fallback here would silently degrade a multi-agent turn into an ordinary
|
|
385
|
+
// one. Fail the turn visibly instead.
|
|
386
|
+
failStream(error);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
332
389
|
try {
|
|
333
390
|
ws.send(frameText);
|
|
334
391
|
sentAt = Date.now();
|
|
@@ -400,8 +457,12 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
400
457
|
return;
|
|
401
458
|
}
|
|
402
459
|
if (!controlFrame && !type.startsWith("response.") && type !== "error") return;
|
|
460
|
+
let steeringEnded = false;
|
|
403
461
|
if (!controlFrame) {
|
|
404
|
-
try {
|
|
462
|
+
try {
|
|
463
|
+
if (nativeControl) steeringEnded = nativeControl.observe(normalized.payload);
|
|
464
|
+
else correlation?.accept(normalized.payload);
|
|
465
|
+
} catch (error) { failStream(error); return; }
|
|
405
466
|
// Correlation must run first: a reused socket's foreign-stream error settles as a
|
|
406
467
|
// non-replayable 502 above, never as the refused-create 4xx projection below, which
|
|
407
468
|
// is the one status family that could authorize an account replay.
|
|
@@ -443,7 +504,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
443
504
|
return;
|
|
444
505
|
}
|
|
445
506
|
if (!controlFrame) relayedEvents += 1;
|
|
446
|
-
if (type === "response.completed" || type === "response.failed" || type === "response.incomplete" || type === "error") {
|
|
507
|
+
if (nativeControl ? steeringEnded : (type === "response.completed" || type === "response.failed" || type === "response.incomplete" || type === "error")) {
|
|
447
508
|
const completedId = correlation?.completed(normalized.payload) ?? null;
|
|
448
509
|
terminal = true;
|
|
449
510
|
cleanup();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
2
|
+
import { comboFailureDecision } from "../../combos";
|
|
3
|
+
import { httpStatusFromTerminalError } from "../../lib/errors";
|
|
2
4
|
import type { RequestLogContext } from "../request-log";
|
|
3
5
|
import { createSseInspector } from "../relay";
|
|
4
6
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
@@ -30,12 +32,67 @@ const RETRYABLE_ZERO_OUTPUT_INCOMPLETE_REASONS = new Set([
|
|
|
30
32
|
"upstream_stall_timeout",
|
|
31
33
|
]);
|
|
32
34
|
|
|
35
|
+
function bareErrorStatus(payload: unknown): number | undefined {
|
|
36
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
37
|
+
const event = payload as Record<string, unknown>;
|
|
38
|
+
if (event.type !== "error") return undefined;
|
|
39
|
+
const nested = event.error;
|
|
40
|
+
const error = nested && typeof nested === "object" && !Array.isArray(nested)
|
|
41
|
+
? nested as Record<string, unknown>
|
|
42
|
+
: event;
|
|
43
|
+
const explicitStatus = [
|
|
44
|
+
event.status,
|
|
45
|
+
event.status_code,
|
|
46
|
+
event.http_status,
|
|
47
|
+
error.status,
|
|
48
|
+
error.status_code,
|
|
49
|
+
error.http_status,
|
|
50
|
+
]
|
|
51
|
+
.map(value => typeof value === "number" && Number.isInteger(value)
|
|
52
|
+
? value
|
|
53
|
+
: typeof value === "string" && /^\d{3}$/.test(value.trim())
|
|
54
|
+
? Number(value)
|
|
55
|
+
: undefined)
|
|
56
|
+
.find(value => value !== undefined && value >= 400 && value <= 599);
|
|
57
|
+
const code = typeof error.code === "string"
|
|
58
|
+
? error.code
|
|
59
|
+
: typeof event.code === "string" ? event.code : null;
|
|
60
|
+
if (explicitStatus === undefined && code === "invalid_request_error") return 400;
|
|
61
|
+
return explicitStatus ?? httpStatusFromTerminalError({
|
|
62
|
+
type: typeof error.type === "string" && error.type !== "error" ? error.type : undefined,
|
|
63
|
+
code,
|
|
64
|
+
message: typeof error.message === "string"
|
|
65
|
+
? error.message
|
|
66
|
+
: typeof event.message === "string" ? event.message : undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function bareErrorIsRetryable(payload: unknown): boolean {
|
|
71
|
+
const status = bareErrorStatus(payload);
|
|
72
|
+
if (status === undefined || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const event = payload as Record<string, unknown>;
|
|
76
|
+
const nested = event.error;
|
|
77
|
+
const error = nested && typeof nested === "object" && !Array.isArray(nested)
|
|
78
|
+
? nested as Record<string, unknown>
|
|
79
|
+
: event;
|
|
80
|
+
const code = typeof error.code === "string"
|
|
81
|
+
? error.code
|
|
82
|
+
: typeof event.code === "string" ? event.code : null;
|
|
83
|
+
const message = typeof error.message === "string"
|
|
84
|
+
? error.message
|
|
85
|
+
: typeof event.message === "string" ? event.message : "";
|
|
86
|
+
return comboFailureDecision(status, message, { code }) === "hop";
|
|
87
|
+
}
|
|
88
|
+
|
|
33
89
|
function retryableZeroOutputTerminal(payload: unknown): boolean {
|
|
34
90
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
35
91
|
const event = payload as {
|
|
36
92
|
type?: unknown;
|
|
37
93
|
response?: { incomplete_details?: { reason?: unknown } };
|
|
38
94
|
};
|
|
95
|
+
if (bareErrorIsRetryable(event)) return true;
|
|
39
96
|
if (event.type === "response.failed") return true;
|
|
40
97
|
if (event.type !== "response.incomplete") return false;
|
|
41
98
|
const reason = event.response?.incomplete_details?.reason;
|
|
@@ -95,12 +152,17 @@ function failedTerminalResponse(
|
|
|
95
152
|
? nested as Record<string, unknown>
|
|
96
153
|
: {};
|
|
97
154
|
const nestedError = terminalResponse.error;
|
|
155
|
+
const topLevelError = terminalPayload.error;
|
|
98
156
|
const error = nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)
|
|
99
157
|
? nestedError as Record<string, unknown>
|
|
158
|
+
: topLevelError && typeof topLevelError === "object" && !Array.isArray(topLevelError)
|
|
159
|
+
? topLevelError as Record<string, unknown>
|
|
100
160
|
: {
|
|
101
161
|
type: "upstream_error",
|
|
102
162
|
code: "upstream_server_error",
|
|
103
|
-
message:
|
|
163
|
+
message: typeof terminalPayload.message === "string"
|
|
164
|
+
? terminalPayload.message
|
|
165
|
+
: logCtx.upstreamError ?? "Provider stream failed before producing output",
|
|
104
166
|
};
|
|
105
167
|
const headers = new Headers(response.headers);
|
|
106
168
|
headers.set("content-type", "application/json");
|
|
@@ -117,7 +179,7 @@ function failedTerminalResponse(
|
|
|
117
179
|
...(usage && typeof usage === "object" && !Array.isArray(usage) ? { usage } : {}),
|
|
118
180
|
},
|
|
119
181
|
}), {
|
|
120
|
-
status: logCtx.terminalHttpStatus ?? 502,
|
|
182
|
+
status: logCtx.terminalHttpStatus ?? bareErrorStatus(terminalPayload) ?? 502,
|
|
121
183
|
headers,
|
|
122
184
|
});
|
|
123
185
|
}
|
|
@@ -154,11 +216,12 @@ export async function preflightComboStreamResponse(
|
|
|
154
216
|
const inspector = createSseInspector({
|
|
155
217
|
logCtx,
|
|
156
218
|
onParsedPayload: payload => {
|
|
219
|
+
if (terminalStatus !== undefined || outputCommitted || retryableTerminalPayload) return;
|
|
157
220
|
const retryable = retryableTerminal(payload);
|
|
158
221
|
const matchedBareError = retryable && payload !== null && typeof payload === "object"
|
|
159
222
|
&& !Array.isArray(payload) && (payload as { type?: unknown }).type === "error";
|
|
160
|
-
//
|
|
161
|
-
//
|
|
223
|
+
// A zero-output bare error is terminal evidence. Explicit client errors stay
|
|
224
|
+
// committed; unknown and retryable upstream failures may advance the combo.
|
|
162
225
|
if (comboStreamPayloadCommitsOutput(payload) && !matchedBareError) outputCommitted = true;
|
|
163
226
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
|
|
164
227
|
if (retryable) retryableTerminalPayload = payload as Record<string, unknown>;
|
|
@@ -197,7 +260,7 @@ export async function preflightComboStreamResponse(
|
|
|
197
260
|
}
|
|
198
261
|
|
|
199
262
|
// A bare error event is not a protocol terminal (terminalStatus stays undefined),
|
|
200
|
-
// so its
|
|
263
|
+
// so its retryable classification doubles as the terminal evidence.
|
|
201
264
|
if ((terminalStatus === "failed" || terminalStatus === "incomplete"
|
|
202
265
|
|| retryableTerminalPayload?.type === "error")
|
|
203
266
|
&& !outputCommitted && retryableTerminalPayload) {
|
|
@@ -745,6 +745,13 @@ export async function executeComboResponses(
|
|
|
745
745
|
code: failure.upstreamCode,
|
|
746
746
|
message: failure.classificationText,
|
|
747
747
|
});
|
|
748
|
+
// Same target selector as the exclusionary pick below, minus `exclude`: the only
|
|
749
|
+
// difference is deliberate and is the whole point of the single-target retry.
|
|
750
|
+
const retryAfterCooldown = () =>
|
|
751
|
+
pickWithWait({
|
|
752
|
+
eligible: targetEligible,
|
|
753
|
+
now: failureNow,
|
|
754
|
+
});
|
|
748
755
|
if (nextPick) {
|
|
749
756
|
pick = nextPick;
|
|
750
757
|
} else {
|
|
@@ -753,6 +760,25 @@ export async function executeComboResponses(
|
|
|
753
760
|
eligible: targetEligible,
|
|
754
761
|
now: failureNow,
|
|
755
762
|
});
|
|
763
|
+
// A single-target combo with waitForCooldownMs has no alternate target to fail over to,
|
|
764
|
+
// but can recover if it waits for its brief cooldown. The initial attempt accumulated into
|
|
765
|
+
// pick.attempted, so the first pickWithWait above excluded it. retryAfterCooldown below is
|
|
766
|
+
// the same selector with `exclude` deliberately dropped, so the single cooled target
|
|
767
|
+
// becomes eligible again once its cooldown expires.
|
|
768
|
+
// Termination is double-guarded:
|
|
769
|
+
// 1) comboSendScope?.reserveDispatch refuses a second failover hop via comboExecutionBudgetPolicy
|
|
770
|
+
// (maxAlternateTargetSends: 1 for a single declared target).
|
|
771
|
+
// 2) comboTargetsDispatched <= 1 bounds it locally so the retry never loops or waits unnecessarily
|
|
772
|
+
// even if sendBudget scope is absent.
|
|
773
|
+
if (
|
|
774
|
+
!pick
|
|
775
|
+
&& combo.targets.length === 1
|
|
776
|
+
&& combo.waitForCooldownMs > 0
|
|
777
|
+
&& comboTargetsDispatched <= 1
|
|
778
|
+
&& !options.abortSignal?.aborted
|
|
779
|
+
) {
|
|
780
|
+
pick = await retryAfterCooldown();
|
|
781
|
+
}
|
|
756
782
|
}
|
|
757
783
|
if (!pick) {
|
|
758
784
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativeResponseControl } from "./native-response-control";
|
|
1
2
|
import type { OcxUsage, OcxProviderContinuationState, OcxConfig } from "../../types";
|
|
2
3
|
import type { CodexAuthPolicyConfig, CodexAuthContext } from "../../codex/auth-context";
|
|
3
4
|
import type { AdmissionLease } from "../../lib/admission";
|
|
@@ -51,6 +52,8 @@ export interface HandleResponsesOptions {
|
|
|
51
52
|
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
52
53
|
onRequestBodyRead?: () => void;
|
|
53
54
|
forceEmptyResponseId?: boolean;
|
|
55
|
+
/** Internal, connection-owned control channel; never reconstructed from headers. */
|
|
56
|
+
nativeControl?: NativeResponseControl;
|
|
54
57
|
abortSignal?: AbortSignal;
|
|
55
58
|
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
56
59
|
onFirstOutput?: () => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativeResponseControl } from "./native-response-control";
|
|
1
2
|
import type { Server } from "bun";
|
|
2
3
|
import {
|
|
3
4
|
codexWsUpstreamFetch,
|
|
@@ -53,6 +54,7 @@ export interface PaceAwareFetch {
|
|
|
53
54
|
export type ProviderFetch = typeof globalThis.fetch & PaceAwareFetch;
|
|
54
55
|
|
|
55
56
|
export interface ProviderFetchOptions {
|
|
57
|
+
nativeControl?: NativeResponseControl;
|
|
56
58
|
providerName?: string;
|
|
57
59
|
modelId?: string;
|
|
58
60
|
/** One pacing slot was acquired immediately before this fetch wrapper was created. */
|
|
@@ -101,7 +103,8 @@ export function providerFetch(
|
|
|
101
103
|
// used, protocol pin included: a WS turn that falls back is serving the
|
|
102
104
|
// request over HTTP, and dropping the provider's `upstreamHttpVersion`
|
|
103
105
|
// there would silently negotiate a transport the operator ruled out.
|
|
104
|
-
return codexWsUpstreamFetch(input, init, httpFetch, runtime, options.onCodexWsQuota, options.beforeDispatch
|
|
106
|
+
return codexWsUpstreamFetch(input, init, httpFetch, runtime, options.onCodexWsQuota, options.beforeDispatch, options.nativeControl,
|
|
107
|
+
() => waitForPacing(init.signal ?? undefined));
|
|
105
108
|
}
|
|
106
109
|
return httpFetch(input, init);
|
|
107
110
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { nativeResponseRecord as injectionRecord } from "./native-response-json";
|
|
2
|
+
export { nativeResponseRecord as injectionRecord, nativeResponseFingerprint as injectionFingerprint } from "./native-response-json";
|
|
3
|
+
import { CODEX_WS_ID_MAX_BYTES } from "./codex-ws-correlation";
|
|
4
|
+
import { NativeSteeringError } from "./native-steering";
|
|
5
|
+
|
|
6
|
+
export type InjectionFrame = Record<string, unknown>;
|
|
7
|
+
export type FunctionResult = { type: "function_call_output"; call_id: string; output: string };
|
|
8
|
+
export const MAX_NATIVE_INJECTIONS = 32;
|
|
9
|
+
export const MAX_NATIVE_INJECTION_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
export const MAX_NATIVE_INJECTION_CALLS = 1024;
|
|
11
|
+
export const NATIVE_INJECTION_ACK_MS = 90_000;
|
|
12
|
+
export const NATIVE_INJECTION_TOOL_MS = 30 * 60_000;
|
|
13
|
+
|
|
14
|
+
/** Bound identities and exclude control characters, without changing their spelling. */
|
|
15
|
+
export function injectionId(value: unknown): value is string {
|
|
16
|
+
return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= CODEX_WS_ID_MAX_BYTES
|
|
17
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
18
|
+
}
|
|
19
|
+
/** Throw only fixed, content-free errors, never tool output or caller identifiers. */
|
|
20
|
+
export function injectionError(code: string, message: string): never {
|
|
21
|
+
throw new NativeSteeringError(code, message);
|
|
22
|
+
}
|
|
23
|
+
/** First-version contract: nonempty arrays of string-valued, client-owned function results. */
|
|
24
|
+
export function injectionResults(value: unknown): FunctionResult[] {
|
|
25
|
+
if (!Array.isArray(value) || !value.length || value.length > MAX_NATIVE_INJECTION_CALLS) {
|
|
26
|
+
injectionError("invalid_injection", "Supply a bounded, nonempty array of saved function results.");
|
|
27
|
+
}
|
|
28
|
+
const seen = new Set<string>();
|
|
29
|
+
for (const item of value) {
|
|
30
|
+
if (!injectionRecord(item) || item.type !== "function_call_output" || !injectionId(item.call_id)
|
|
31
|
+
|| typeof item.output !== "string" || Object.keys(item).some(key => !["type", "call_id", "output"].includes(key))) {
|
|
32
|
+
injectionError("invalid_injection", "Only function_call_output with call_id and string output is supported; no messages or hosted-tool results.");
|
|
33
|
+
}
|
|
34
|
+
if (seen.has(item.call_id)) injectionError("duplicate_injection", "A function result must occur exactly once.");
|
|
35
|
+
seen.add(item.call_id);
|
|
36
|
+
}
|
|
37
|
+
return value as FunctionResult[];
|
|
38
|
+
}
|
|
39
|
+
/** Detect explicit multi-agent opt-in without inferring it from the model name. */
|
|
40
|
+
export function isInjectionRequest(frame: InjectionFrame): boolean {
|
|
41
|
+
return injectionRecord(frame.multi_agent) && frame.multi_agent.enabled === true;
|
|
42
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { MAX_NATIVE_STEERING_REPLAY_BYTES, type NativeSteeringReplayObserver } from "./native-steering-replay";
|
|
2
|
+
import { injectionRecord as record, type InjectionFrame as Frame, type FunctionResult } from "./native-injection-protocol";
|
|
3
|
+
|
|
4
|
+
import { nativeResultFingerprint } from "./native-tool-results";
|
|
5
|
+
import { nativeResponseOutput } from "./native-response-output";
|
|
6
|
+
|
|
7
|
+
/** A bounded journal of accepted tool results, independent of user steering history. */
|
|
8
|
+
export class NativeInjectionReplay implements NativeSteeringReplayObserver {
|
|
9
|
+
private prefix: unknown[];
|
|
10
|
+
private bytes = 0;
|
|
11
|
+
private current?: string;
|
|
12
|
+
private output = new Map<number, Frame>();
|
|
13
|
+
private accepted = new Map<string, FunctionResult>();
|
|
14
|
+
private pending?: FunctionResult[];
|
|
15
|
+
private pendingBytes = 0;
|
|
16
|
+
private acceptedBatchBytes: number[] = [];
|
|
17
|
+
private explicit: unknown[] = [];
|
|
18
|
+
private previous: unknown[] = [];
|
|
19
|
+
|
|
20
|
+
/** Capture a private initial prefix; existing persistence eligibility is checked by the caller. */
|
|
21
|
+
constructor(input: unknown, private readonly remember: (input: unknown[], response: Frame) => void) {
|
|
22
|
+
this.prefix = typeof input === "string"
|
|
23
|
+
? [{ type: "message", role: "user", content: [{ type: "input_text", text: input }] }]
|
|
24
|
+
: Array.isArray(input) ? [...input] : [];
|
|
25
|
+
this.reserve(this.prefix);
|
|
26
|
+
}
|
|
27
|
+
/** Charge serialized bytes, refusing rather than truncating an over-budget transcript. */
|
|
28
|
+
private reserve(value: unknown): number {
|
|
29
|
+
const bytes = Buffer.byteLength(JSON.stringify(value));
|
|
30
|
+
if (this.bytes + bytes > MAX_NATIVE_STEERING_REPLAY_BYTES) throw new Error("Native injection replay exceeded its history budget.");
|
|
31
|
+
this.bytes += bytes;
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
/** Journal before physical send, with rollback usable only for a known unsent frame. */
|
|
35
|
+
submitted(frame: Frame): () => void {
|
|
36
|
+
const input = Array.isArray(frame.input) ? structuredClone(frame.input) : [];
|
|
37
|
+
const bytes = this.reserve(input);
|
|
38
|
+
if (frame.type === "response.inject") { this.pending = input as FunctionResult[]; this.pendingBytes = bytes; }
|
|
39
|
+
else this.explicit = input;
|
|
40
|
+
return () => {
|
|
41
|
+
if (frame.type === "response.inject") { this.pending = undefined; this.pendingBytes = 0; }
|
|
42
|
+
else this.explicit = [];
|
|
43
|
+
this.bytes -= bytes;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Keep wire output order and insert each accepted result after its owning function call. */
|
|
47
|
+
private completedOutput(response: Frame): unknown[] {
|
|
48
|
+
const output = nativeResponseOutput(this.output, response.output);
|
|
49
|
+
const echoed = new Set<string>();
|
|
50
|
+
for (const item of output) {
|
|
51
|
+
if (!record(item) || item.type !== "function_call_output" || typeof item.call_id !== "string") continue;
|
|
52
|
+
const accepted = this.accepted.get(item.call_id);
|
|
53
|
+
if (accepted) {
|
|
54
|
+
if (echoed.has(item.call_id) || nativeResultFingerprint(item as FunctionResult) !== nativeResultFingerprint(accepted)) throw new Error("Native injection replay result mismatch.");
|
|
55
|
+
echoed.add(item.call_id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const merged: unknown[] = [];
|
|
59
|
+
const found = new Set(echoed);
|
|
60
|
+
for (const item of output) {
|
|
61
|
+
merged.push(item);
|
|
62
|
+
if (!record(item) || item.type !== "function_call" || typeof item.call_id !== "string") continue;
|
|
63
|
+
const accepted = this.accepted.get(item.call_id);
|
|
64
|
+
if (accepted && !found.has(item.call_id)) { merged.push(accepted); found.add(item.call_id); }
|
|
65
|
+
}
|
|
66
|
+
if (found.size !== this.accepted.size) throw new Error("Native injection replay is missing an accepted result's call.");
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
69
|
+
/** Terminals are supplied by the owner only after all injection acknowledgements settle. */
|
|
70
|
+
observe(frame: Frame): void {
|
|
71
|
+
if (frame.type === "response.created") {
|
|
72
|
+
if (this.current) {
|
|
73
|
+
for (const item of this.previous) this.prefix.push(item);
|
|
74
|
+
for (const item of this.explicit) this.prefix.push(item);
|
|
75
|
+
}
|
|
76
|
+
this.current = String(record(frame.response) ? frame.response.id : "");
|
|
77
|
+
this.output.clear(); this.accepted.clear(); this.acceptedBatchBytes = []; this.explicit = []; this.previous = [];
|
|
78
|
+
} else if (frame.type === "response.inject.created" || frame.type === "response.inject.failed") {
|
|
79
|
+
if (!this.pending) throw new Error("Native injection replay acknowledgement has no pending input.");
|
|
80
|
+
if (frame.type === "response.inject.created") {
|
|
81
|
+
for (const item of this.pending) this.accepted.set(item.call_id, item);
|
|
82
|
+
this.acceptedBatchBytes.push(this.pendingBytes);
|
|
83
|
+
} else this.bytes -= this.pendingBytes;
|
|
84
|
+
this.pending = undefined; this.pendingBytes = 0;
|
|
85
|
+
} else if (frame.type === "response.output_item.done") {
|
|
86
|
+
if (!Number.isSafeInteger(frame.output_index) || (frame.output_index as number) < 0
|
|
87
|
+
|| (frame.output_index as number) > 10_000 || !record(frame.item)) throw new Error("Native injection replay output identity is invalid.");
|
|
88
|
+
const old = this.output.get(frame.output_index as number);
|
|
89
|
+
if (old) this.bytes -= Buffer.byteLength(JSON.stringify(old));
|
|
90
|
+
this.reserve(frame.item); this.output.set(frame.output_index as number, structuredClone(frame.item));
|
|
91
|
+
} else if (record(frame.response) && ["response.completed", "response.failed", "response.incomplete"].includes(String(frame.type))) {
|
|
92
|
+
if (this.pending) throw new Error("Native injection replay cannot commit an unacknowledged result.");
|
|
93
|
+
const output = this.completedOutput(frame.response);
|
|
94
|
+
for (const item of this.output.values()) this.bytes -= Buffer.byteLength(JSON.stringify(item));
|
|
95
|
+
for (const bytes of this.acceptedBatchBytes) this.bytes -= bytes;
|
|
96
|
+
this.reserve(output); this.output.clear(); this.accepted.clear(); this.acceptedBatchBytes = []; this.previous = output;
|
|
97
|
+
if (frame.type === "response.completed") this.remember(this.prefix, { ...frame.response, output });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Drop all retained bodies at cancellation, connection teardown or unknown delivery. */
|
|
101
|
+
dispose(): void {
|
|
102
|
+
this.prefix = []; this.output.clear(); this.accepted.clear(); this.pending = undefined; this.pendingBytes = 0; this.acceptedBatchBytes = [];
|
|
103
|
+
this.explicit = []; this.previous = []; this.bytes = 0;
|
|
104
|
+
}
|
|
105
|
+
}
|