@bitkyc08/opencodex 2.62.0-preview.20260923 → 2.63.0-preview.20260923

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/gui/dist/assets/App-EJxiUFMq.js +50 -0
  2. package/gui/dist/assets/{Tray-DdvPBVLi.js → Tray-B03pW-Uf.js} +1 -1
  3. package/gui/dist/assets/index-BmJwNBHL.js +86 -0
  4. package/gui/dist/assets/index-DdDunwDb.css +1 -0
  5. package/gui/dist/assets/{usage-companion-chart-BIhvlbQW.js → usage-companion-chart-IftE60UK.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/cursor/catalog.ts +15 -0
  9. package/src/adapters/cursor/effort-map.ts +7 -0
  10. package/src/adapters/cursor/envelope-echo.ts +51 -25
  11. package/src/adapters/cursor/protobuf-request.ts +39 -13
  12. package/src/adapters/cursor.ts +52 -4
  13. package/src/adapters/devin/cloud-direct/chat.ts +50 -16
  14. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +4 -1
  15. package/src/adapters/devin/live-models.ts +7 -0
  16. package/src/adapters/devin.ts +7 -5
  17. package/src/adapters/kiro/reasoning.ts +5 -0
  18. package/src/adapters/openai-responses/passthrough.ts +4 -4
  19. package/src/adapters/openai-responses/tool-output-recovery.ts +5 -3
  20. package/src/claude/desktop-gateway-state.ts +7 -2
  21. package/src/claude/desktop-policy.ts +105 -1
  22. package/src/cli/config-command.ts +22 -7
  23. package/src/cli/doctor.ts +14 -4
  24. package/src/codex/catalog/effort.ts +35 -4
  25. package/src/codex/catalog/metadata.ts +33 -5
  26. package/src/codex/catalog/native-models.ts +43 -2
  27. package/src/codex/catalog/pinned-models.ts +37 -0
  28. package/src/codex/catalog-auto-refresh.ts +6 -0
  29. package/src/codex/data/roster-pinned-models.json +359 -0
  30. package/src/codex/data/upstream-models.json +365 -271
  31. package/src/codex/inject/provider-table.ts +108 -0
  32. package/src/codex/inject/remove.ts +2 -114
  33. package/src/codex/model-entitlements.ts +14 -10
  34. package/src/codex/subagent-defaults.ts +2 -109
  35. package/src/codex/toml-source-lines.ts +112 -0
  36. package/src/config/live-reconcile.ts +145 -27
  37. package/src/config/load-degrade.ts +3 -4
  38. package/src/config.ts +2 -2
  39. package/src/generated/compatibility-version.json +83 -67
  40. package/src/generated/model-metadata.ts +5 -5
  41. package/src/lab/artifacts/sanitize.ts +60 -19
  42. package/src/lib/app-owned-memory-stores.ts +7 -0
  43. package/src/lib/app-owned-memory.ts +13 -2
  44. package/src/lib/errors.ts +6 -4
  45. package/src/lib/upstream-retry.ts +35 -5
  46. package/src/oauth/devin.ts +43 -37
  47. package/src/providers/codebuddy-models.ts +11 -0
  48. package/src/providers/kiro-models.ts +9 -0
  49. package/src/providers/quota/vendor-probes-oauth.ts +9 -2
  50. package/src/providers/registry/entries-core.ts +19 -8
  51. package/src/providers/registry/entries-extended.ts +4 -1
  52. package/src/providers/registry/model-seeds.ts +22 -2
  53. package/src/responses/bridge-search-replay-cache.ts +20 -10
  54. package/src/responses/plaintext-v2-agent-messages.ts +10 -1
  55. package/src/routing/identity-domains.ts +22 -15
  56. package/src/server/index/websocket-handler.ts +22 -2
  57. package/src/server/management/agent-settings-routes.ts +19 -10
  58. package/src/server/management/context.ts +4 -2
  59. package/src/server/responses/codex-ws-exchange.ts +24 -8
  60. package/src/server/responses/core-codex-account.ts +4 -0
  61. package/src/server/responses/core-combo-failure.ts +16 -9
  62. package/src/server/responses/core-combo.ts +7 -3
  63. package/src/server/responses/core-options.ts +6 -0
  64. package/src/server/responses/native-injection-replay.ts +13 -1
  65. package/src/server/responses/native-injection.ts +66 -7
  66. package/src/server/responses/native-response-control.ts +6 -2
  67. package/src/server/responses/native-steering-replay.ts +60 -0
  68. package/src/server/responses/native-steering.ts +9 -1
  69. package/src/server/responses/passthrough-delivery.ts +3 -4
  70. package/src/server/responses/passthrough-dispatch.ts +7 -0
  71. package/src/server/responses/request-prepare.ts +12 -0
  72. package/src/server/responses/request-transport.ts +5 -2
  73. package/src/server/responses/ws-upstream.ts +1 -1
  74. package/src/server/ws-bridge.ts +17 -1
  75. package/src/types/request.ts +5 -0
  76. package/src/usage/expected-prices.ts +38 -8
  77. package/src/web-search/executor.ts +38 -13
  78. package/gui/dist/assets/App-CANWata-.js +0 -50
  79. package/gui/dist/assets/index-BIS5HXEP.js +0 -86
  80. 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: true };
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.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile };
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 => recordClaudeDesktopMode(persisted, desktopMode));
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
- recordClaudeDesktopMode(config, desktopMode);
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, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
1172
- const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({ platform: deps.platform ?? process.platform });
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, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
1223
- const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({
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 { probeClaudeDesktopPolicy } from "../../claude/desktop-policy";
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?: typeof 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
- 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));
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
- if (frame.type === "response.create") continuationBase = outgoing;
368
- try { ws.send(text); } catch {
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
- void beforeContinuation().then(sendControl).catch(() => failStream("Native steering continuation could not be dispatched; do not automatically replay queued input"));
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: formatErrorResponse(
102
- response.status,
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
- const failureDecision = comboFailureDecision(failure.response.status, failure.classificationText, {
682
- code: failure.upstreamCode,
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
- try { this.recordTerminal(); const copy = JSON.parse(JSON.stringify(frame)) as Frame; this.replay?.submitted(copy); this.send(copy); }
201
- catch { this.fail(); injectionError("injection_delivery_unknown", "Continuation delivery is unknown; do not automatically resend results."); }
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; public API controls require an explicit provider WebSocket opt-in. */
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 (control?.kind === "injection" || control?.kind === "steering") && provider.adapter === "openai-responses"
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
- // Scope the executed-search memo to this exact upstream (#4587). The Responses adapter
421
- // derives the same scope from the same base URL before the NEXT turn is dispatched, so
422
- // a replayed hosted cell can be turned back into the destination's own call and result.
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