@bitkyc08/opencodex 2.62.0-preview.20260923 → 2.63.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-BqrsSrIR.js +50 -0
- package/gui/dist/assets/{Tray-DdvPBVLi.js → Tray-CncKDBTp.js} +1 -1
- package/gui/dist/assets/index-C6SJrh0N.js +86 -0
- package/gui/dist/assets/index-DdDunwDb.css +1 -0
- package/gui/dist/assets/{usage-companion-chart-BIhvlbQW.js → usage-companion-chart-CzAAAB1o.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +15 -0
- package/src/adapters/cursor/effort-map.ts +7 -0
- package/src/adapters/cursor/envelope-echo.ts +51 -25
- package/src/adapters/cursor/protobuf-request.ts +39 -13
- package/src/adapters/cursor.ts +52 -4
- package/src/adapters/devin/cloud-direct/chat.ts +50 -16
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +4 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/devin.ts +7 -5
- package/src/adapters/kiro/reasoning.ts +5 -0
- package/src/adapters/openai-responses/passthrough.ts +4 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +5 -3
- package/src/claude/desktop-gateway-state.ts +7 -2
- package/src/claude/desktop-policy.ts +105 -1
- package/src/cli/config-command.ts +22 -7
- package/src/cli/doctor.ts +14 -4
- package/src/codex/catalog/effort.ts +35 -4
- package/src/codex/catalog/metadata.ts +33 -5
- package/src/codex/catalog/native-models.ts +43 -2
- package/src/codex/catalog/pinned-models.ts +37 -0
- package/src/codex/catalog-auto-refresh.ts +6 -0
- package/src/codex/data/roster-pinned-models.json +359 -0
- package/src/codex/data/upstream-models.json +365 -271
- package/src/codex/inject/provider-table.ts +108 -0
- package/src/codex/inject/remove.ts +2 -114
- package/src/codex/model-entitlements.ts +14 -10
- package/src/codex/subagent-defaults.ts +2 -109
- package/src/codex/toml-source-lines.ts +112 -0
- package/src/config/live-reconcile.ts +145 -27
- package/src/config/load-degrade.ts +3 -4
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +83 -67
- package/src/generated/model-metadata.ts +5 -5
- package/src/lab/artifacts/sanitize.ts +60 -19
- package/src/lib/app-owned-memory-stores.ts +7 -0
- package/src/lib/app-owned-memory.ts +13 -2
- package/src/lib/errors.ts +6 -4
- package/src/lib/upstream-retry.ts +35 -5
- package/src/oauth/devin.ts +43 -37
- package/src/providers/codebuddy-models.ts +11 -0
- package/src/providers/kiro-models.ts +9 -0
- package/src/providers/quota/vendor-probes-oauth.ts +9 -2
- package/src/providers/registry/entries-core.ts +19 -8
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +22 -2
- package/src/responses/bridge-search-replay-cache.ts +20 -10
- package/src/responses/plaintext-v2-agent-messages.ts +10 -1
- package/src/routing/identity-domains.ts +22 -15
- package/src/server/index/websocket-handler.ts +22 -2
- package/src/server/management/agent-settings-routes.ts +19 -10
- package/src/server/management/context.ts +4 -2
- package/src/server/responses/codex-ws-exchange.ts +24 -8
- package/src/server/responses/core-codex-account.ts +4 -0
- package/src/server/responses/core-combo-failure.ts +16 -9
- package/src/server/responses/core-combo.ts +7 -3
- package/src/server/responses/core-options.ts +6 -0
- package/src/server/responses/native-injection-replay.ts +13 -1
- package/src/server/responses/native-injection.ts +66 -7
- package/src/server/responses/native-response-control.ts +6 -2
- package/src/server/responses/native-steering-replay.ts +60 -0
- package/src/server/responses/native-steering.ts +9 -1
- package/src/server/responses/passthrough-delivery.ts +3 -4
- package/src/server/responses/passthrough-dispatch.ts +7 -0
- package/src/server/responses/request-prepare.ts +12 -0
- package/src/server/responses/request-transport.ts +5 -2
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/ws-bridge.ts +17 -1
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +38 -8
- package/src/web-search/executor.ts +38 -13
- package/gui/dist/assets/App-CANWata-.js +0 -50
- package/gui/dist/assets/index-BIS5HXEP.js +0 -86
- package/gui/dist/assets/index-_bpvxJu0.css +0 -1
|
@@ -7,6 +7,7 @@ import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../co
|
|
|
7
7
|
import { MULTI_AGENT_SURFACE_ADVISORY_VERSION, multiAgentSurfaceAdvisory, resolveMultiAgentMode } from "../../config/multi-agent-surface";
|
|
8
8
|
import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
|
|
9
9
|
import {
|
|
10
|
+
adoptPersistedClaudeCode,
|
|
10
11
|
DEFAULT_SUBAGENT_MODELS,
|
|
11
12
|
codexAutoStartEnabled,
|
|
12
13
|
deleteConfigTopLevelKey,
|
|
@@ -123,12 +124,12 @@ function persistDesktopProfileField(
|
|
|
123
124
|
): { ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" } {
|
|
124
125
|
const outcome = mutatePersistedConfig(persisted => {
|
|
125
126
|
persisted.claudeCode = { ...(persisted.claudeCode ?? {}), desktopProfile };
|
|
126
|
-
return { changed: true, value:
|
|
127
|
+
return { changed: true, value: structuredClone(persisted.claudeCode) };
|
|
127
128
|
});
|
|
128
129
|
// Only mirror into memory once the durable write actually landed; an
|
|
129
130
|
// `unavailable` outcome must not leave the snapshot claiming a saved profile.
|
|
130
131
|
if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
|
|
131
|
-
config
|
|
132
|
+
adoptPersistedClaudeCode(config, outcome.value);
|
|
132
133
|
return { ok: true };
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -137,9 +138,15 @@ async function persistDesktopModeField(
|
|
|
137
138
|
desktopMode: "first-party" | "gateway",
|
|
138
139
|
): Promise<{ ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" }> {
|
|
139
140
|
const { recordClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
140
|
-
const outcome = mutatePersistedConfig(persisted =>
|
|
141
|
+
const outcome = mutatePersistedConfig(persisted => {
|
|
142
|
+
const mutation = recordClaudeDesktopMode(persisted, desktopMode);
|
|
143
|
+
return { changed: mutation.changed, value: structuredClone(persisted.claudeCode) };
|
|
144
|
+
});
|
|
141
145
|
if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
|
|
142
|
-
|
|
146
|
+
// First-party apply ends here — no profile-marker write follows — so without
|
|
147
|
+
// adopting, live diverges from the armed baseline and a later whole-config
|
|
148
|
+
// save reads that divergence as a pending mutation and stomps hand edits.
|
|
149
|
+
adoptPersistedClaudeCode(config, outcome.value);
|
|
143
150
|
return { ok: true };
|
|
144
151
|
}
|
|
145
152
|
|
|
@@ -1168,8 +1175,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1168
1175
|
...(modeWarning ? { warning: modeWarning } : {}),
|
|
1169
1176
|
}, 500);
|
|
1170
1177
|
}
|
|
1171
|
-
const { claudeDesktopPolicyWarning,
|
|
1172
|
-
const policyState =
|
|
1178
|
+
const { claudeDesktopPolicyWarning, getCachedClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
|
|
1179
|
+
const policyState = deps.probeClaudeDesktopPolicy
|
|
1180
|
+
? await deps.probeClaudeDesktopPolicy({ platform: deps.platform ?? process.platform })
|
|
1181
|
+
: await getCachedClaudeDesktopPolicy({ platform: deps.platform ?? process.platform });
|
|
1173
1182
|
const policyWarning = claudeDesktopPolicyWarning(policyState);
|
|
1174
1183
|
const warning = [modeWarning, policyWarning].filter(Boolean).join(" ");
|
|
1175
1184
|
return jsonResponse({
|
|
@@ -1219,10 +1228,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1219
1228
|
// a stale apply the operator should refresh.
|
|
1220
1229
|
const stale = desiredEnabled && (mode === "first-party" ? firstPartySeen.stale : observed.kind === "gateway_drifted");
|
|
1221
1230
|
const { getDesktopHealth } = await import("../../claude/desktop-health");
|
|
1222
|
-
const { claudeDesktopPolicyHealth,
|
|
1223
|
-
const policyState =
|
|
1224
|
-
platform: deps.platform ?? process.platform
|
|
1225
|
-
|
|
1231
|
+
const { claudeDesktopPolicyHealth, getCachedClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
|
|
1232
|
+
const policyState = deps.probeClaudeDesktopPolicy
|
|
1233
|
+
? await deps.probeClaudeDesktopPolicy({ platform: deps.platform ?? process.platform })
|
|
1234
|
+
: await getCachedClaudeDesktopPolicy({ platform: deps.platform ?? process.platform });
|
|
1226
1235
|
// Managed-policy conflicts only matter for the gateway profile; first-party mode never
|
|
1227
1236
|
// touches Desktop's own configuration.
|
|
1228
1237
|
const policy = claudeDesktopPolicyHealth(mode === "first-party" ? "absent" : policyState);
|
|
@@ -10,7 +10,7 @@ import type { refreshOwnedCatalogIntegrations } from "../../integrations/catalog
|
|
|
10
10
|
import type { Paths as CodexPromptPaths } from "../../codex/prompt-layers";
|
|
11
11
|
import type { injectGrokConfig } from "../../grok/inject";
|
|
12
12
|
import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
13
|
-
import type {
|
|
13
|
+
import type { ClaudeDesktopPolicyProbeOptions, ClaudeDesktopPolicyState } from "../../claude/desktop-policy";
|
|
14
14
|
import type { RuntimePortState } from "../../config/process-state";
|
|
15
15
|
import type { CursorInstall } from "../../integrations/cursor-detect";
|
|
16
16
|
import type { CursorEffortTable } from "../../integrations/cursor-effort-table";
|
|
@@ -74,7 +74,9 @@ export interface ManagementApiDeps {
|
|
|
74
74
|
removeDesktop3pStandardPivot?: typeof removeDesktop3pStandardPivot;
|
|
75
75
|
writeDesktop3pConfig?: typeof writeDesktop3pConfig;
|
|
76
76
|
/** Read-only Windows MDM policy seam for status/apply tests. */
|
|
77
|
-
probeClaudeDesktopPolicy?:
|
|
77
|
+
probeClaudeDesktopPolicy?: (
|
|
78
|
+
options?: ClaudeDesktopPolicyProbeOptions,
|
|
79
|
+
) => ClaudeDesktopPolicyState | Promise<ClaudeDesktopPolicyState>;
|
|
78
80
|
/**
|
|
79
81
|
* Runtime-state seam: the fence must name the host/port the RUNNING process
|
|
80
82
|
* bound (agent-settings-routes.ts:99-103 pattern), and a test must not depend
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NativeSteeringError } from "./native-steering";
|
|
1
2
|
import { mergeSteeringContinuation } from "./native-steering-settings";
|
|
2
3
|
import { markNativeControlResponse } from "./native-response-control";
|
|
3
4
|
import type { NativeResponseControl } from "./native-response-control";
|
|
@@ -345,11 +346,11 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
345
346
|
// the first create frame.
|
|
346
347
|
let base: Record<string, unknown> | undefined;
|
|
347
348
|
detachSteering = nativeControl.attach(frame => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
// Build, serialize and bound-check the reconstructed frame synchronously in the
|
|
350
|
+
// channel callback: a typed refusal must reach the channel's synchronous rollback
|
|
351
|
+
// (continuation slot released, journal unwritten) rather than the asynchronous
|
|
352
|
+
// failStream path, so a corrected continuation can still retry on this channel.
|
|
353
|
+
const prepare = () => {
|
|
353
354
|
let outgoing = frame;
|
|
354
355
|
if (frame.type === "response.create") {
|
|
355
356
|
// Generation overrides have passed route policy; identity/tools remain pinned.
|
|
@@ -361,11 +362,20 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
361
362
|
: { ...continuationBase, input: frame.input, previous_response_id: frame.previous_response_id };
|
|
362
363
|
}
|
|
363
364
|
const text = JSON.stringify(outgoing);
|
|
365
|
+
nativeControl.assertOutboundFrame?.(text);
|
|
364
366
|
if (codexWsCreateFrameExceedsLimit(text)) {
|
|
365
367
|
throw new Error("Native steering frame exceeds the transport byte limit");
|
|
366
368
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
+
return { outgoing, text };
|
|
370
|
+
};
|
|
371
|
+
const prepared = prepare();
|
|
372
|
+
const sendControl = () => {
|
|
373
|
+
if (terminal || signal?.aborted || session.closed || ws.readyState !== WebSocket.OPEN) {
|
|
374
|
+
throw new Error("Native steering connection is no longer available");
|
|
375
|
+
}
|
|
376
|
+
beforeDispatch?.(new Headers(headers));
|
|
377
|
+
if (frame.type === "response.create") continuationBase = prepared.outgoing;
|
|
378
|
+
try { ws.send(prepared.text); } catch {
|
|
369
379
|
// A send failure has unknown delivery. Never replay or fall back.
|
|
370
380
|
failStream("Native steering send failed; delivery is unknown");
|
|
371
381
|
throw new Error("Native steering send failed; delivery is unknown");
|
|
@@ -374,7 +384,13 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
374
384
|
if (frame.type === "response.create" && beforeContinuation) {
|
|
375
385
|
// Explicit tool-result continuations are physical request starts;
|
|
376
386
|
// they keep provider pacing and revalidate auth AFTER the wait.
|
|
377
|
-
|
|
387
|
+
// A typed pre-send refusal (e.g. the configured upstream body limit) is a
|
|
388
|
+
// known non-delivery and keeps its own message; every other failure keeps
|
|
389
|
+
// the unknown-delivery wording.
|
|
390
|
+
void beforeContinuation().then(sendControl).catch(error => failStream(
|
|
391
|
+
error instanceof NativeSteeringError ? error
|
|
392
|
+
: new Error("Native steering continuation could not be dispatched; do not automatically replay queued input"),
|
|
393
|
+
));
|
|
378
394
|
} else sendControl();
|
|
379
395
|
}, error => failStream(error));
|
|
380
396
|
}
|
|
@@ -212,6 +212,10 @@ export async function codexPoolAccountModel400Denial(
|
|
|
212
212
|
wireModelId?: string,
|
|
213
213
|
): Promise<string | undefined> {
|
|
214
214
|
if (response.status !== 400) return undefined;
|
|
215
|
+
// A response that must not be sent again cannot open an alternate-account retry either. The
|
|
216
|
+
// reset helper marks the answer to a spent operator replacement this way, and that turn may
|
|
217
|
+
// already have run on the first send. Same rule as the quota and transient ladders below.
|
|
218
|
+
if (isNonReplayableResponse(response)) return undefined;
|
|
215
219
|
try {
|
|
216
220
|
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
217
221
|
if (!body.displaySafe || body.truncated) return undefined;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { normalizeUpstreamErrorText } from "./core-errors";
|
|
14
14
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
15
15
|
import { formatErrorResponse } from "../../bridge";
|
|
16
|
+
import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
16
17
|
import { usageFromResponsesPayload } from "../request-log";
|
|
17
18
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
18
19
|
|
|
@@ -30,6 +31,9 @@ export async function consumeComboFailure(
|
|
|
30
31
|
signal?: AbortSignal,
|
|
31
32
|
now = Date.now(),
|
|
32
33
|
): Promise<ConsumedComboFailure> {
|
|
34
|
+
// Read before the body: the marker lives on this Response object, and the failure below is
|
|
35
|
+
// rebuilt as a new one that would otherwise lose it.
|
|
36
|
+
const nonReplayable = isNonReplayableResponse(response);
|
|
33
37
|
const fallback = `Provider error ${response.status}`;
|
|
34
38
|
let classificationText = fallback;
|
|
35
39
|
let usage: OcxUsage | undefined;
|
|
@@ -97,16 +101,19 @@ export async function consumeComboFailure(
|
|
|
97
101
|
now,
|
|
98
102
|
includeDefault: false,
|
|
99
103
|
});
|
|
104
|
+
const failureResponse = formatErrorResponse(
|
|
105
|
+
response.status,
|
|
106
|
+
cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
107
|
+
message,
|
|
108
|
+
{
|
|
109
|
+
...(normalizedUpstreamCode !== undefined ? { code: normalizedUpstreamCode } : {}),
|
|
110
|
+
...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
if (nonReplayable) markResponseNonReplayable(failureResponse);
|
|
100
114
|
return {
|
|
101
|
-
response:
|
|
102
|
-
|
|
103
|
-
cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
104
|
-
message,
|
|
105
|
-
{
|
|
106
|
-
...(normalizedUpstreamCode !== undefined ? { code: normalizedUpstreamCode } : {}),
|
|
107
|
-
...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
|
|
108
|
-
},
|
|
109
|
-
),
|
|
115
|
+
response: failureResponse,
|
|
116
|
+
...(nonReplayable ? { nonReplayable: true } : {}),
|
|
110
117
|
classificationText,
|
|
111
118
|
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
112
119
|
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
@@ -678,9 +678,13 @@ export async function executeComboResponses(
|
|
|
678
678
|
attemptRetained = true;
|
|
679
679
|
lastFailure = failure.response;
|
|
680
680
|
lastFailedChildLog = childLog;
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
681
|
+
// A non-replayable failure (the answer to a spent ambiguous-reset replacement) may follow a
|
|
682
|
+
// send that already ran the turn, so no later target may receive it, whatever its status says.
|
|
683
|
+
const failureDecision = failure.nonReplayable
|
|
684
|
+
? "stop"
|
|
685
|
+
: comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
686
|
+
code: failure.upstreamCode,
|
|
687
|
+
});
|
|
684
688
|
const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
|
|
685
689
|
// Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
|
|
686
690
|
const classifyOverflow = failure.response.status === 413
|
|
@@ -28,6 +28,12 @@ export interface ConsumedComboFailure {
|
|
|
28
28
|
resetAt?: string[];
|
|
29
29
|
/** Reserved for 040 usage attribution without adding another body read. */
|
|
30
30
|
usage?: OcxUsage;
|
|
31
|
+
/**
|
|
32
|
+
* The failed attempt's response was marked non-replayable, such as the answer to a spent
|
|
33
|
+
* ambiguous-reset replacement. The re-wrapped {@link response} cannot carry that in-memory
|
|
34
|
+
* marker, so the combo loop reads it here and stops instead of sending the turn to a later target.
|
|
35
|
+
*/
|
|
36
|
+
nonReplayable?: boolean;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_NATIVE_STEERING_REPLAY_BYTES, type NativeSteeringReplayObserver } from "./native-steering-replay";
|
|
1
|
+
import { MAX_NATIVE_STEERING_REPLAY_BYTES, admitNativeControlReplayJournal, registerNativeControlReplayJournal, type NativeSteeringReplayObserver } from "./native-steering-replay";
|
|
2
2
|
import { injectionRecord as record, type InjectionFrame as Frame, type FunctionResult } from "./native-injection-protocol";
|
|
3
3
|
|
|
4
4
|
import { nativeResultFingerprint } from "./native-tool-results";
|
|
@@ -16,6 +16,9 @@ export class NativeInjectionReplay implements NativeSteeringReplayObserver {
|
|
|
16
16
|
private acceptedBatchBytes: number[] = [];
|
|
17
17
|
private explicit: unknown[] = [];
|
|
18
18
|
private previous: unknown[] = [];
|
|
19
|
+
private unregisterAccounting?: () => void;
|
|
20
|
+
|
|
21
|
+
get retainedBytes(): number { return this.bytes; }
|
|
19
22
|
|
|
20
23
|
/** Capture a private initial prefix; existing persistence eligibility is checked by the caller. */
|
|
21
24
|
constructor(input: unknown, private readonly remember: (input: unknown[], response: Frame) => void) {
|
|
@@ -23,12 +26,19 @@ export class NativeInjectionReplay implements NativeSteeringReplayObserver {
|
|
|
23
26
|
? [{ type: "message", role: "user", content: [{ type: "input_text", text: input }] }]
|
|
24
27
|
: Array.isArray(input) ? [...input] : [];
|
|
25
28
|
this.reserve(this.prefix);
|
|
29
|
+
this.unregisterAccounting = registerNativeControlReplayJournal(this);
|
|
26
30
|
}
|
|
27
31
|
/** Charge serialized bytes, refusing rather than truncating an over-budget transcript. */
|
|
28
32
|
private reserve(value: unknown): number {
|
|
29
33
|
const bytes = Buffer.byteLength(JSON.stringify(value));
|
|
30
34
|
if (this.bytes + bytes > MAX_NATIVE_STEERING_REPLAY_BYTES) throw new Error("Native injection replay exceeded its history budget.");
|
|
31
35
|
this.bytes += bytes;
|
|
36
|
+
try {
|
|
37
|
+
admitNativeControlReplayJournal(this);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
this.bytes -= bytes;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
32
42
|
return bytes;
|
|
33
43
|
}
|
|
34
44
|
/** Journal before physical send, with rollback usable only for a known unsent frame. */
|
|
@@ -99,6 +109,8 @@ export class NativeInjectionReplay implements NativeSteeringReplayObserver {
|
|
|
99
109
|
}
|
|
100
110
|
/** Drop all retained bodies at cancellation, connection teardown or unknown delivery. */
|
|
101
111
|
dispose(): void {
|
|
112
|
+
this.unregisterAccounting?.();
|
|
113
|
+
this.unregisterAccounting = undefined;
|
|
102
114
|
this.prefix = []; this.output.clear(); this.accepted.clear(); this.pending = undefined; this.pendingBytes = 0; this.acceptedBatchBytes = [];
|
|
103
115
|
this.explicit = []; this.previous = []; this.bytes = 0;
|
|
104
116
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { CodexWsCorrelation } from "./codex-ws-correlation";
|
|
2
|
+
import { NativeSteeringError } from "./native-steering";
|
|
3
|
+
import { checkOutboundBodySize } from "./outbound-body-guard";
|
|
2
4
|
import type { NativeResponseControl } from "./native-response-control";
|
|
3
5
|
import type { NativeSteeringReplayObserver } from "./native-steering-replay";
|
|
6
|
+
import {
|
|
7
|
+
UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
8
|
+
undeclaredToolCallMessage,
|
|
9
|
+
undeclaredToolCallNameInResponse,
|
|
10
|
+
} from "../responses-undeclared-tool-guard";
|
|
11
|
+
import type { ProviderExecutedCallType } from "../responses-undeclared-tool-guard";
|
|
4
12
|
import {
|
|
5
13
|
injectionError, injectionFingerprint, injectionId, injectionRecord as record, injectionResults,
|
|
6
14
|
isInjectionRequest, MAX_NATIVE_INJECTIONS, MAX_NATIVE_INJECTION_BYTES, MAX_NATIVE_INJECTION_CALLS,
|
|
@@ -44,20 +52,39 @@ export class NativeInjectionChannel implements NativeResponseControl {
|
|
|
44
52
|
private ackTimer?: ReturnType<typeof setTimeout>;
|
|
45
53
|
private idleTimer?: ReturnType<typeof setTimeout>;
|
|
46
54
|
private readonly settings = new Map<string, string>();
|
|
55
|
+
private declaredToolNames?: ReadonlySet<string>;
|
|
56
|
+
private declaredBareToolNames: ReadonlySet<string> = new Set();
|
|
57
|
+
private declaredNamelessCallTypes: ReadonlySet<string> = new Set();
|
|
58
|
+
private providerExecutedCallTypes: ReadonlySet<ProviderExecutedCallType> = new Set();
|
|
47
59
|
private readonly lane: unknown;
|
|
48
60
|
|
|
49
61
|
/** Pin the original settings and lane; construction never opens a connection. */
|
|
50
62
|
constructor(initial: Frame, private readonly idleMs = 300_000,
|
|
63
|
+
private readonly maxUpstreamBodyBytes?: number,
|
|
51
64
|
private readonly deadlines = { ackMs: NATIVE_INJECTION_ACK_MS, toolMs: NATIVE_INJECTION_TOOL_MS }) {
|
|
52
65
|
if (!isInjectionRequest(initial)) injectionError("injection_not_supported", "Native injection requires explicit multi_agent.enabled.");
|
|
53
66
|
this.lane = initial.stream_id ?? undefined;
|
|
54
67
|
for (const [key, value] of Object.entries(initial)) if (!ENVELOPE.has(key)) this.settings.set(key, injectionFingerprint(value));
|
|
55
68
|
}
|
|
69
|
+
/** Refuse the exact rebuilt control body before it reaches the retained socket. */
|
|
70
|
+
assertOutboundFrame(text: string): void {
|
|
71
|
+
if (!checkOutboundBodySize(text, this.maxUpstreamBodyBytes).admitted) {
|
|
72
|
+
injectionError("outbound_body_too_large", "Native injection frame exceeds the configured upstream body limit.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
56
75
|
/** Report that the real dispatch boundary has selected this owner. */
|
|
57
76
|
get attached(): boolean { return this.everAttached; }
|
|
58
77
|
/** A terminal is not final until submitted results have acknowledgements. */
|
|
59
78
|
get ended(): boolean { return this.finished; }
|
|
60
79
|
|
|
80
|
+
/** Mirror the ordinary response guard for native events that bypass its SSE rewrite. */
|
|
81
|
+
configureToolAuthorization(active: boolean, names: ReadonlySet<string>, bareNames: ReadonlySet<string>, namelessCallTypes: ReadonlySet<string>, providerExecuted: ReadonlySet<ProviderExecutedCallType>): void {
|
|
82
|
+
this.declaredToolNames = active ? new Set(names) : undefined;
|
|
83
|
+
this.declaredBareToolNames = active ? new Set(bareNames) : new Set();
|
|
84
|
+
this.declaredNamelessCallTypes = active ? new Set(namelessCallTypes) : new Set();
|
|
85
|
+
this.providerExecutedCallTypes = active ? new Set(providerExecuted) : new Set();
|
|
86
|
+
}
|
|
87
|
+
|
|
61
88
|
/** Attach once, after routing/auth/admission, retaining no global response-ID lookup. */
|
|
62
89
|
attach(send: (frame: Frame) => void, fail: (error: Error) => void): () => void {
|
|
63
90
|
if (this.everAttached) throw new Error("Native injection transport is already owned.");
|
|
@@ -77,17 +104,28 @@ export class NativeInjectionChannel implements NativeResponseControl {
|
|
|
77
104
|
return injectionError("native_control_mode_mismatch", "This multi-agent turn owns an injection-only channel; start a separate turn for steering.");
|
|
78
105
|
}
|
|
79
106
|
/** Abort unknown-delivery state without HTTP fallback, resends or invented acceptance. */
|
|
80
|
-
private fail(): void {
|
|
107
|
+
private fail(error?: Error): void {
|
|
81
108
|
this.finished = true;
|
|
82
109
|
clearTimeout(this.ackTimer); clearTimeout(this.idleTimer);
|
|
83
|
-
this.onFailure?.(new Error("Native injection transport failed or timed out; delivery is unknown. Do not automatically resend or rerun tools."));
|
|
110
|
+
this.onFailure?.(error ?? new Error("Native injection transport failed or timed out; delivery is unknown. Do not automatically resend or rerun tools."));
|
|
84
111
|
}
|
|
85
112
|
/** Require the same live owner; an unbound or detached channel cannot authorize a send. */
|
|
86
113
|
private live(): void {
|
|
87
114
|
if (!this.send || this.finished) injectionError("injection_not_supported", "No live native injection transport is available on this route.");
|
|
88
115
|
}
|
|
116
|
+
private authorize(item: unknown): void {
|
|
117
|
+
if (!this.declaredToolNames) return;
|
|
118
|
+
const undeclared = undeclaredToolCallNameInResponse(
|
|
119
|
+
{ output: [item] }, this.declaredToolNames, this.declaredNamelessCallTypes,
|
|
120
|
+
this.providerExecutedCallTypes, this.declaredBareToolNames,
|
|
121
|
+
);
|
|
122
|
+
if (undeclared !== undefined) {
|
|
123
|
+
injectionError(UNDECLARED_TOOL_CALL_ERROR_CODE, undeclaredToolCallMessage(undeclared));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
89
126
|
/** Advertise client-owned function/custom calls and approvals, never hosted execution. */
|
|
90
127
|
private advertise(item: unknown): void {
|
|
128
|
+
this.authorize(item);
|
|
91
129
|
const requirement = nativeToolRequirement(item);
|
|
92
130
|
if (!requirement) return;
|
|
93
131
|
const old = this.calls.get(requirement.key);
|
|
@@ -116,6 +154,7 @@ export class NativeInjectionChannel implements NativeResponseControl {
|
|
|
116
154
|
if (call.state !== "available") injectionError("duplicate_injection", "This function result was already submitted; do not replay it.");
|
|
117
155
|
}
|
|
118
156
|
const text = JSON.stringify(frame);
|
|
157
|
+
this.assertOutboundFrame(text);
|
|
119
158
|
const bytes = Buffer.byteLength(text);
|
|
120
159
|
if (this.queue.length >= MAX_NATIVE_INJECTIONS || bytes + this.queueBytes > MAX_NATIVE_INJECTION_BYTES) {
|
|
121
160
|
injectionError("injection_queue_full", "Native injection queue count or byte limit reached; no result was sent.");
|
|
@@ -135,10 +174,23 @@ export class NativeInjectionChannel implements NativeResponseControl {
|
|
|
135
174
|
this.inFlight = submission;
|
|
136
175
|
this.ackTimer = setTimeout(() => this.fail(), this.deadlines.ackMs);
|
|
137
176
|
this.ackTimer.unref?.();
|
|
177
|
+
let rollback: (() => void) | undefined;
|
|
138
178
|
try {
|
|
139
|
-
this.replay?.submitted(submission.frame);
|
|
179
|
+
rollback = this.replay?.submitted(submission.frame);
|
|
140
180
|
this.send!(submission.frame);
|
|
141
|
-
} catch {
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error instanceof NativeSteeringError) {
|
|
183
|
+
// A typed refusal is a known non-delivery: unjournal and free the reservation
|
|
184
|
+
// so a corrected result can be queued again on the same channel.
|
|
185
|
+
rollback?.();
|
|
186
|
+
clearTimeout(this.ackTimer); this.ackTimer = undefined;
|
|
187
|
+
this.inFlight = undefined; this.queue.shift(); this.queueBytes -= submission.bytes;
|
|
188
|
+
for (const item of submission.results) {
|
|
189
|
+
const call = this.calls.get(nativeResultKey(item));
|
|
190
|
+
if (call?.state === "queued") call.state = "available";
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
142
194
|
this.fail();
|
|
143
195
|
injectionError("injection_delivery_unknown", "Injection dispatch failed; do not automatically resend or rerun tools.");
|
|
144
196
|
}
|
|
@@ -163,7 +215,7 @@ export class NativeInjectionChannel implements NativeResponseControl {
|
|
|
163
215
|
clearTimeout(this.ackTimer); this.ackTimer = undefined;
|
|
164
216
|
this.inFlight = undefined; this.queue.shift(); this.queueBytes -= pending.bytes;
|
|
165
217
|
// Do not let a synchronous fake peer publish the next ack before this event is relayed.
|
|
166
|
-
if (this.queue.length) queueMicrotask(() => { try { this.pump(); } catch { this.fail(); } });
|
|
218
|
+
if (this.queue.length) queueMicrotask(() => { try { this.pump(); } catch (error) { this.fail(error instanceof NativeSteeringError ? error : undefined); } });
|
|
167
219
|
}
|
|
168
220
|
/** Commit terminal replay only when no submitted injection can change its accepted inputs. */
|
|
169
221
|
private recordTerminal(): void {
|
|
@@ -197,8 +249,14 @@ export class NativeInjectionChannel implements NativeResponseControl {
|
|
|
197
249
|
}
|
|
198
250
|
if (Buffer.byteLength(JSON.stringify(frame)) > MAX_NATIVE_INJECTION_BYTES) injectionError("invalid_injection", "Native injection continuation exceeds its byte limit.");
|
|
199
251
|
this.continuationSent = true;
|
|
200
|
-
|
|
201
|
-
|
|
252
|
+
let undo: (() => void) | undefined;
|
|
253
|
+
try { this.recordTerminal(); const copy = JSON.parse(JSON.stringify(frame)) as Frame; undo = this.replay?.submitted(copy); this.send(copy); }
|
|
254
|
+
catch (error) {
|
|
255
|
+
// A typed refusal is a known non-delivery: unjournal, release the continuation
|
|
256
|
+
// slot so a corrected frame can be sent, and keep the code.
|
|
257
|
+
if (error instanceof NativeSteeringError) { undo?.(); this.continuationSent = false; throw error; }
|
|
258
|
+
this.fail(); injectionError("injection_delivery_unknown", "Continuation delivery is unknown; do not automatically resend results.");
|
|
259
|
+
}
|
|
202
260
|
if (!this.finished) this.armIdle(this.deadlines.ackMs);
|
|
203
261
|
return true;
|
|
204
262
|
}
|
|
@@ -225,6 +283,7 @@ export class NativeInjectionChannel implements NativeResponseControl {
|
|
|
225
283
|
this.correlation?.finish(); this.correlation = new CodexWsCorrelation(true, () => false);
|
|
226
284
|
} else if (!this.currentId || this.terminal) throw new Error("Unexpected native injection event outside an active response.");
|
|
227
285
|
this.correlation?.accept({ ...event, stream_id: undefined });
|
|
286
|
+
if (type === "response.output_item.added") this.authorize(event.item);
|
|
228
287
|
if (type === "response.output_item.done") this.advertise(event.item);
|
|
229
288
|
if (["response.completed", "response.failed", "response.incomplete"].includes(String(type))) {
|
|
230
289
|
if (!this.currentId || response?.id !== this.currentId) throw new Error("Native injection terminal identity mismatch.");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../../types";
|
|
2
2
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
3
3
|
import type { NativeSteeringReplayObserver } from "./native-steering-replay";
|
|
4
|
+
import type { ProviderExecutedCallType } from "../responses-undeclared-tool-guard";
|
|
4
5
|
|
|
5
6
|
import { isInjectionRequest } from "./native-injection-protocol";
|
|
6
7
|
|
|
@@ -10,6 +11,7 @@ export interface NativeResponseControl {
|
|
|
10
11
|
relayActive: boolean;
|
|
11
12
|
normalizeContinuation?: (frame: Record<string, unknown>) => Record<string, unknown>;
|
|
12
13
|
replayFactory?: () => NativeSteeringReplayObserver;
|
|
14
|
+
configureToolAuthorization?: (active: boolean, names: ReadonlySet<string>, bareNames: ReadonlySet<string>, namelessCallTypes: ReadonlySet<string>, providerExecuted: ReadonlySet<ProviderExecutedCallType>) => void;
|
|
13
15
|
readonly attached: boolean;
|
|
14
16
|
readonly ended: boolean;
|
|
15
17
|
attach(send: (frame: Record<string, unknown>) => void, fail: (error: Error) => void): () => void;
|
|
@@ -17,6 +19,8 @@ export interface NativeResponseControl {
|
|
|
17
19
|
steer(frame: Record<string, unknown>): void;
|
|
18
20
|
inject?(frame: Record<string, unknown>): void;
|
|
19
21
|
continue(frame: Record<string, unknown>): boolean;
|
|
22
|
+
/** Apply operator-configured limits to the exact reconstructed upstream frame. */
|
|
23
|
+
assertOutboundFrame?(text: string): void;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export const OPENAI_API_RESPONSES_URL = "https://api.openai.com/v1/responses";
|
|
@@ -27,10 +31,10 @@ export function markNativeControlResponse(response: Response): Response { native
|
|
|
27
31
|
/** Recognize a marked native response by identity, not by caller-controlled content. */
|
|
28
32
|
export function isNativeControlResponse(response: Response): boolean { return nativeControlResponses.has(response); }
|
|
29
33
|
|
|
30
|
-
/** Preserve canonical ChatGPT eligibility;
|
|
34
|
+
/** Preserve canonical ChatGPT eligibility; only injection may use the separately billed public API. */
|
|
31
35
|
export function nativeResponseControlEligible(provider: OcxProviderConfig, control?: NativeResponseControl): boolean {
|
|
32
36
|
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
33
|
-
return
|
|
37
|
+
return control?.kind === "injection" && provider.adapter === "openai-responses"
|
|
34
38
|
&& provider.upstreamWebsocket === true && provider.authMode !== "forward"
|
|
35
39
|
&& provider.baseUrl?.replace(/\/+$/, "") === "https://api.openai.com/v1";
|
|
36
40
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { nativeResponseOutput } from "./native-response-output";
|
|
2
|
+
import {
|
|
3
|
+
admitAppOwnedPinnedBytes,
|
|
4
|
+
appOwnedBytesSnapshot,
|
|
5
|
+
type RetainedStoreSnapshot,
|
|
6
|
+
} from "../../lib/app-owned-memory";
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Connection-local replay journal. Only input committed by response.created enters
|
|
@@ -6,6 +11,46 @@ import { nativeResponseOutput } from "./native-response-output";
|
|
|
6
11
|
* continuation cache. Bodies are bounded and discarded at connection teardown.
|
|
7
12
|
*/
|
|
8
13
|
export const MAX_NATIVE_STEERING_REPLAY_BYTES = 32 * 1024 * 1024;
|
|
14
|
+
/**
|
|
15
|
+
* Aggregate ceiling across every live steering and injection journal. The operator
|
|
16
|
+
* budget is an eviction target that can be raised to 4 GiB; pinned control journals
|
|
17
|
+
* keep their own finite admission cap so the documented pin-capable aggregate stays
|
|
18
|
+
* below the process-owned 512 MiB worst case.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_NATIVE_CONTROL_REPLAY_TOTAL_BYTES = 128 * 1024 * 1024;
|
|
21
|
+
let aggregateCapBytes = MAX_NATIVE_CONTROL_REPLAY_TOTAL_BYTES;
|
|
22
|
+
const activeReplays = new Set<{ readonly retainedBytes: number }>();
|
|
23
|
+
|
|
24
|
+
/** Account active journals as pinned state: protocol safety forbids evicting pending input. */
|
|
25
|
+
export function nativeControlReplayRetainedStoreSnapshot(): RetainedStoreSnapshot {
|
|
26
|
+
let bytes = 0;
|
|
27
|
+
for (const replay of activeReplays) bytes += replay.retainedBytes;
|
|
28
|
+
return { count: activeReplays.size, bytes, evictableBytes: 0, pinnedBytes: bytes, oldestAt: null };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Track one live control journal (steering or injection) and return its detach hook. */
|
|
32
|
+
export function registerNativeControlReplayJournal(journal: { readonly retainedBytes: number }): () => void {
|
|
33
|
+
activeReplays.add(journal);
|
|
34
|
+
return () => { activeReplays.delete(journal); };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shared pinned-memory admission for one journal's current retained bytes. Bytes the
|
|
39
|
+
* retained-store registry already sees are measured in place; a journal still in its
|
|
40
|
+
* constructor is priced as a new proposal. Reclaimable owners are demoted before
|
|
41
|
+
* refusal, so cache occupancy alone never fails a journal.
|
|
42
|
+
*/
|
|
43
|
+
export function admitNativeControlReplayJournal(journal: { readonly retainedBytes: number }): void {
|
|
44
|
+
const replayBytes = nativeControlReplayRetainedStoreSnapshot().bytes
|
|
45
|
+
+ (activeReplays.has(journal) ? 0 : journal.retainedBytes);
|
|
46
|
+
if (replayBytes > aggregateCapBytes) {
|
|
47
|
+
throw new Error("Native control replay exceeded the pinned journal ceiling.");
|
|
48
|
+
}
|
|
49
|
+
const registeredBytes = appOwnedBytesSnapshot().stores.native_control_replay?.bytes ?? 0;
|
|
50
|
+
if (!admitAppOwnedPinnedBytes(replayBytes - registeredBytes)) {
|
|
51
|
+
throw new Error("Native control replay exceeded the application-owned memory budget.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
9
54
|
type Frame = Record<string, unknown>;
|
|
10
55
|
/** Accept JSON object envelopes without treating arrays as records. */
|
|
11
56
|
function record(value: unknown): value is Frame {
|
|
@@ -32,16 +77,22 @@ export class NativeSteeringReplay implements NativeSteeringReplayObserver {
|
|
|
32
77
|
private submissions: Array<{ parent: string; input: unknown[]; id?: string; bytes: number }> = [];
|
|
33
78
|
private explicitInput: unknown[] = [];
|
|
34
79
|
private explicitBytes = 0;
|
|
80
|
+
private disposed = false;
|
|
81
|
+
private unregisterAccounting?: () => void;
|
|
82
|
+
|
|
83
|
+
get retainedBytes(): number { return this.bytes; }
|
|
35
84
|
|
|
36
85
|
/** Capture the initial prefix and reject over-budget history before dispatch. */
|
|
37
86
|
constructor(input: unknown, private readonly remember: (input: unknown[], response: Frame) => void) {
|
|
38
87
|
this.prefix = [...inputItems(input)];
|
|
39
88
|
this.bytes = Buffer.byteLength(JSON.stringify(this.prefix));
|
|
40
89
|
this.check();
|
|
90
|
+
this.unregisterAccounting = registerNativeControlReplayJournal(this);
|
|
41
91
|
}
|
|
42
92
|
/** Reject overflow rather than silently truncating retained conversation input. */
|
|
43
93
|
private check(): void {
|
|
44
94
|
if (this.bytes > MAX_NATIVE_STEERING_REPLAY_BYTES) throw new Error("Native steering replay exceeded its bounded history budget; input was not silently truncated.");
|
|
95
|
+
admitNativeControlReplayJournal(this);
|
|
45
96
|
}
|
|
46
97
|
/** Reserve replay bytes before send and return a rollback for synchronous failure. */
|
|
47
98
|
submitted(frame: Frame): () => void {
|
|
@@ -116,6 +167,10 @@ export class NativeSteeringReplay implements NativeSteeringReplayObserver {
|
|
|
116
167
|
}
|
|
117
168
|
/** Release retained input, output and queued submissions when the owner detaches. */
|
|
118
169
|
dispose(): void {
|
|
170
|
+
if (this.disposed) return;
|
|
171
|
+
this.disposed = true;
|
|
172
|
+
this.unregisterAccounting?.();
|
|
173
|
+
this.unregisterAccounting = undefined;
|
|
119
174
|
this.prefix = [];
|
|
120
175
|
this.previousOutput = [];
|
|
121
176
|
this.submissions = [];
|
|
@@ -124,3 +179,8 @@ export class NativeSteeringReplay implements NativeSteeringReplayObserver {
|
|
|
124
179
|
this.bytes = 0;
|
|
125
180
|
}
|
|
126
181
|
}
|
|
182
|
+
|
|
183
|
+
/** Test-only: shrink the aggregate pinned-journal ceiling (null restores the documented cap). */
|
|
184
|
+
export function setNativeControlReplayTotalCapForTests(capBytes: number | null): void {
|
|
185
|
+
aggregateCapBytes = capBytes ?? MAX_NATIVE_CONTROL_REPLAY_TOTAL_BYTES;
|
|
186
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { NativeSteeringReplayObserver } from "./native-steering-replay";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { CODEX_WS_ID_MAX_BYTES, CodexWsCorrelation } from "./codex-ws-correlation";
|
|
5
5
|
import { codexWsCreateFrameExceedsLimit } from "./codex-ws-wire";
|
|
6
|
+
import { checkOutboundBodySize } from "./outbound-body-guard";
|
|
6
7
|
|
|
7
8
|
export const MAX_NATIVE_STEERS = 32;
|
|
8
9
|
export const MAX_NATIVE_STEERING_RESPONSES = 128;
|
|
@@ -123,7 +124,7 @@ export class NativeSteeringChannel {
|
|
|
123
124
|
private advertisedBytes = 0;
|
|
124
125
|
|
|
125
126
|
/** Pin the initial lane and setting digests without opening a transport. */
|
|
126
|
-
constructor(initial: Frame, private readonly idleMs = 300_000) {
|
|
127
|
+
constructor(initial: Frame, private readonly idleMs = 300_000, private readonly maxUpstreamBodyBytes?: number) {
|
|
127
128
|
if (record(initial.multi_agent) && initial.multi_agent.enabled === true) {
|
|
128
129
|
throw new NativeSteeringError("native_control_mode_mismatch", "Multi-agent responses cannot use the single-agent steering channel.");
|
|
129
130
|
}
|
|
@@ -132,6 +133,13 @@ export class NativeSteeringChannel {
|
|
|
132
133
|
if (!["type", "input", "previous_response_id", "stream", "stream_id"].includes(key)) this.settings.set(key, fingerprint(value));
|
|
133
134
|
}
|
|
134
135
|
}
|
|
136
|
+
|
|
137
|
+
/** Refuse the exact rebuilt control body before it reaches the retained socket. */
|
|
138
|
+
assertOutboundFrame(text: string): void {
|
|
139
|
+
if (!checkOutboundBodySize(text, this.maxUpstreamBodyBytes).admitted) {
|
|
140
|
+
throw new NativeSteeringError("outbound_body_too_large", "Native steering frame exceeds the configured upstream body limit.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
135
143
|
/** Report whether native dispatch ever bound a physical connection to this owner. */
|
|
136
144
|
get attached(): boolean { return this.everAttached; }
|
|
137
145
|
/** Report whether ordered terminal handling has finished the native chain. */
|
|
@@ -417,10 +417,9 @@ export async function deliverPassthroughResponse(
|
|
|
417
417
|
describeImages: requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName),
|
|
418
418
|
sidecar: config.webSearchSidecar,
|
|
419
419
|
}),
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
destinationScope: bridgeSearchReplayScope(route.provider.baseUrl),
|
|
420
|
+
// Snapshot the bound conversation, provider, model, destination, and credential. The
|
|
421
|
+
// next turn must match every dimension before its hosted cell can recover this result.
|
|
422
|
+
destinationScope: bridgeSearchReplayScope(parsed._reasoningReplayScope),
|
|
424
423
|
// Appending a search result can push the continuation past the ceiling the first leg
|
|
425
424
|
// was admitted under, so the same limit is re-applied before every later send.
|
|
426
425
|
checkOutboundBody: (continuationBody: string) => {
|
|
@@ -480,6 +480,13 @@ export async function preparePassthroughExchange(
|
|
|
480
480
|
|| clientDeclaredNamelessCallTypes.size > 0
|
|
481
481
|
|| clientExplicitWireToolCatalog
|
|
482
482
|
) && route.provider.authMode !== "forward";
|
|
483
|
+
options.nativeControl?.configureToolAuthorization?.(
|
|
484
|
+
undeclaredToolGuardActive,
|
|
485
|
+
declaredWireToolNames,
|
|
486
|
+
declaredBareWireToolNames,
|
|
487
|
+
declaredNamelessClientCallTypes,
|
|
488
|
+
providerExecutedCallTypes,
|
|
489
|
+
);
|
|
483
490
|
};
|
|
484
491
|
refreshUndeclaredToolGuard(request);
|
|
485
492
|
// A refused turn must not seed `previous_response_id` replay. The inspection branch reads the
|