@bitkyc08/opencodex 2.57.0 → 2.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +2 -2
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
  10. package/src/adapters/command-code.ts +13 -4
  11. package/src/adapters/cursor/catalog.ts +11 -0
  12. package/src/adapters/cursor/cursor-errors.ts +15 -0
  13. package/src/adapters/cursor/discovery.ts +65 -1
  14. package/src/adapters/cursor/effort-map.ts +16 -2
  15. package/src/adapters/cursor/envelope-echo.ts +55 -2
  16. package/src/adapters/cursor/live-transport.ts +5 -1
  17. package/src/adapters/cursor/message-mapper.ts +3 -2
  18. package/src/adapters/cursor/protobuf-events.ts +110 -11
  19. package/src/adapters/cursor/protobuf-request.ts +27 -6
  20. package/src/adapters/cursor/request-builder.ts +14 -3
  21. package/src/adapters/cursor/text-toolcall.ts +230 -0
  22. package/src/adapters/cursor/thread-continuity.ts +141 -0
  23. package/src/adapters/cursor/tool-guidance.ts +5 -4
  24. package/src/adapters/cursor/types.ts +5 -0
  25. package/src/adapters/cursor.ts +97 -6
  26. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  27. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  28. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  29. package/src/adapters/devin.ts +75 -13
  30. package/src/adapters/google-antigravity-wire.ts +29 -2
  31. package/src/adapters/google-http.ts +45 -13
  32. package/src/adapters/google.ts +23 -4
  33. package/src/adapters/mimo-free.ts +32 -17
  34. package/src/adapters/ollama-native.ts +42 -8
  35. package/src/adapters/openai-chat/response-events.ts +61 -0
  36. package/src/adapters/openai-chat.ts +5 -10
  37. package/src/adapters/openai-responses/passthrough.ts +40 -5
  38. package/src/adapters/openai-responses/request-strips.ts +43 -0
  39. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  40. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  41. package/src/adapters/physical-send.ts +50 -0
  42. package/src/adapters/responses-tool-schema.ts +76 -46
  43. package/src/adapters/run-turn-queue.ts +17 -4
  44. package/src/bridge/response-json.ts +2 -2
  45. package/src/bridge/sse.ts +166 -25
  46. package/src/claude/context-windows.ts +22 -0
  47. package/src/claude/outbound.ts +46 -5
  48. package/src/cli/account-api.ts +4 -3
  49. package/src/cli/account-extended.ts +22 -2
  50. package/src/cli/account-orca-import.ts +63 -0
  51. package/src/cli/account.ts +32 -4
  52. package/src/cli/capabilities.ts +40 -0
  53. package/src/cli/claude.ts +29 -1
  54. package/src/cli/codex-cli-update.ts +97 -2
  55. package/src/cli/config-command.ts +35 -18
  56. package/src/cli/dispatch.ts +71 -4
  57. package/src/cli/doctor.ts +197 -2
  58. package/src/cli/help.ts +4 -1
  59. package/src/cli/index.ts +132 -22
  60. package/src/cli/models-runtime.ts +33 -4
  61. package/src/cli/registry.ts +11 -1
  62. package/src/cli/runtime-api.ts +44 -0
  63. package/src/cli/start-args.ts +94 -0
  64. package/src/cli/system-command.ts +72 -1
  65. package/src/cli/uninstall-client-state.ts +12 -0
  66. package/src/client/machine-api.ts +4 -3
  67. package/src/client/machine-listener.ts +14 -1
  68. package/src/clients/config-export/constants.ts +2 -3
  69. package/src/clients/config-export.ts +5 -5
  70. package/src/codex/account-store.ts +81 -5
  71. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  72. package/src/codex/auth-api/routes.ts +17 -2
  73. package/src/codex/auth-context.ts +58 -20
  74. package/src/codex/catalog/build-entries.ts +25 -4
  75. package/src/codex/catalog/derive-entry.ts +8 -1
  76. package/src/codex/catalog/effort.ts +10 -6
  77. package/src/codex/catalog/gather-capture.ts +1 -0
  78. package/src/codex/catalog/model-hints.ts +37 -5
  79. package/src/codex/catalog/parsing.ts +83 -5
  80. package/src/codex/catalog/reserve-warn.ts +96 -0
  81. package/src/codex/catalog/retained-sync.ts +19 -0
  82. package/src/codex/catalog/routed-gather.ts +42 -3
  83. package/src/codex/cli-installation-identity.ts +210 -0
  84. package/src/codex/cli-installation-targets.ts +158 -0
  85. package/src/codex/convergence.ts +5 -0
  86. package/src/codex/desktop-switches.ts +145 -0
  87. package/src/codex/history-job.ts +5 -1
  88. package/src/codex/history-provider.ts +37 -5
  89. package/src/codex/history-state-open.ts +105 -0
  90. package/src/codex/history-worker.ts +14 -1
  91. package/src/codex/inject/config-toml.ts +44 -2
  92. package/src/codex/inject/remove.ts +145 -7
  93. package/src/codex/inject/restore.ts +204 -32
  94. package/src/codex/inject.ts +6 -9
  95. package/src/codex/lineage.ts +83 -32
  96. package/src/codex/loopback-target.ts +40 -0
  97. package/src/codex/main-account-hard-lock.ts +2 -1
  98. package/src/codex/main-account.ts +10 -3
  99. package/src/codex/main-device-reauth.ts +17 -9
  100. package/src/codex/model-entitlements.ts +60 -1
  101. package/src/codex/native-profile-startup.ts +64 -20
  102. package/src/codex/observed-model-denials.ts +137 -0
  103. package/src/codex/orca-auth-source.ts +94 -0
  104. package/src/codex/orca-import.ts +219 -0
  105. package/src/codex/prompt-text-probe.ts +282 -12
  106. package/src/codex/quota-401-recovery.ts +12 -0
  107. package/src/codex/quota-types.ts +65 -0
  108. package/src/codex/quota.ts +24 -19
  109. package/src/codex/routing/cooldown-math.ts +8 -47
  110. package/src/codex/routing/pin-drain.ts +57 -0
  111. package/src/codex/routing.ts +13 -15
  112. package/src/codex/subagent-model-fallback.ts +94 -0
  113. package/src/codex/windows-installation-files.ts +224 -0
  114. package/src/combos/failover.ts +122 -5
  115. package/src/config/atomic-write.ts +83 -8
  116. package/src/config/diagnostics.ts +21 -0
  117. package/src/config/load-degrade.ts +15 -0
  118. package/src/config/pending-teardown.ts +8 -0
  119. package/src/config/process-state.ts +36 -3
  120. package/src/config/provider-relative-send-path.ts +16 -0
  121. package/src/config/proxy-env.ts +23 -5
  122. package/src/config/schema/config-schema.ts +23 -0
  123. package/src/config/schema/leaf-validators.ts +65 -17
  124. package/src/generated/compatibility-version.json +337 -201
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/lib/bounded-body.ts +4 -2
  127. package/src/lib/bounded-subprocess.ts +62 -10
  128. package/src/lib/destination-policy.ts +48 -6
  129. package/src/lib/errors.ts +3 -15
  130. package/src/lib/local-destinations.ts +32 -5
  131. package/src/lib/provider-outbound.ts +3 -3
  132. package/src/lib/proxy-env.ts +70 -3
  133. package/src/lib/request-execution-budget.ts +11 -3
  134. package/src/lib/response-body-inactivity.ts +193 -0
  135. package/src/lib/retry-delay.ts +69 -0
  136. package/src/lib/socks5-fetch.ts +631 -0
  137. package/src/lib/spend-reservation-ledger.ts +115 -9
  138. package/src/lib/windows-secret-acl.ts +151 -15
  139. package/src/lib/windows-user-principal.ts +5 -1
  140. package/src/lib/workflow-budget.ts +145 -8
  141. package/src/oauth/account-quota-rank.ts +72 -15
  142. package/src/oauth/generic-account-failover.ts +40 -27
  143. package/src/oauth/orcarouter.ts +15 -2
  144. package/src/oauth/store.ts +8 -0
  145. package/src/providers/codex-capacity.ts +9 -0
  146. package/src/providers/derive.ts +6 -0
  147. package/src/providers/devin-provider-merge-migration.ts +33 -12
  148. package/src/providers/free-directory.ts +20 -2
  149. package/src/providers/key-failover.ts +261 -7
  150. package/src/providers/model-discovery.ts +19 -7
  151. package/src/providers/model-rename-migration.ts +1 -0
  152. package/src/providers/openai-sidecar.ts +4 -0
  153. package/src/providers/opencode-go-transport.ts +14 -5
  154. package/src/providers/quota/report-cache.ts +3 -0
  155. package/src/providers/registry/entries-core.ts +11 -0
  156. package/src/providers/registry/entries-extended.ts +146 -28
  157. package/src/providers/registry/model-seeds.ts +136 -29
  158. package/src/providers/registry/types.ts +9 -0
  159. package/src/responses/apply-patch-envelope.ts +44 -11
  160. package/src/responses/bridge-search-replay-cache.ts +152 -0
  161. package/src/responses/code-mode-helper-compat.ts +26 -16
  162. package/src/responses/custom-tool-compat.ts +1 -1
  163. package/src/responses/hosted-tool-policy.ts +85 -2
  164. package/src/responses/schema.ts +9 -2
  165. package/src/responses/spill-store.ts +17 -0
  166. package/src/responses/state/body-policy.ts +25 -0
  167. package/src/responses/state/spill-queue.ts +8 -6
  168. package/src/responses/state.ts +3 -22
  169. package/src/router.ts +4 -0
  170. package/src/server/auth-cors.ts +27 -0
  171. package/src/server/chat-completions.ts +9 -4
  172. package/src/server/chat-native-sse.ts +26 -9
  173. package/src/server/chat-native.ts +10 -4
  174. package/src/server/claude-messages.ts +24 -2
  175. package/src/server/gui-static.ts +36 -2
  176. package/src/server/inbound-body-admission.ts +187 -0
  177. package/src/server/index/websocket-handler.ts +48 -1
  178. package/src/server/index.ts +15 -19
  179. package/src/server/management/api-access.ts +3 -4
  180. package/src/server/management/config-routes.ts +57 -10
  181. package/src/server/management/provider-capability-config.ts +35 -7
  182. package/src/server/management/provider-routes.ts +70 -18
  183. package/src/server/models-capabilities.ts +24 -3
  184. package/src/server/proxy-liveness.ts +97 -2
  185. package/src/server/relay.ts +17 -24
  186. package/src/server/request-log.ts +25 -1
  187. package/src/server/responses/adapter-continuation.ts +71 -27
  188. package/src/server/responses/adapter-delivery.ts +39 -8
  189. package/src/server/responses/adapter-dispatch.ts +52 -24
  190. package/src/server/responses/codex-ws-exchange.ts +65 -4
  191. package/src/server/responses/combo-stream-preflight.ts +68 -5
  192. package/src/server/responses/compact.ts +60 -11
  193. package/src/server/responses/core-codex-account.ts +83 -22
  194. package/src/server/responses/core-combo.ts +26 -0
  195. package/src/server/responses/core-normalize.ts +12 -5
  196. package/src/server/responses/core-options.ts +3 -0
  197. package/src/server/responses/fetch-helpers.ts +72 -3
  198. package/src/server/responses/native-injection-protocol.ts +42 -0
  199. package/src/server/responses/native-injection-replay.ts +105 -0
  200. package/src/server/responses/native-injection.ts +242 -0
  201. package/src/server/responses/native-response-control.ts +56 -0
  202. package/src/server/responses/native-response-json.ts +14 -0
  203. package/src/server/responses/native-response-output.ts +37 -0
  204. package/src/server/responses/native-steering-log.ts +44 -0
  205. package/src/server/responses/native-steering-policy.ts +49 -0
  206. package/src/server/responses/native-steering-replay.ts +126 -0
  207. package/src/server/responses/native-steering-settings.ts +76 -0
  208. package/src/server/responses/native-steering.ts +400 -0
  209. package/src/server/responses/native-tool-results.ts +130 -0
  210. package/src/server/responses/passthrough-delivery.ts +21 -1
  211. package/src/server/responses/passthrough-dispatch.ts +146 -49
  212. package/src/server/responses/passthrough-execution.ts +11 -1
  213. package/src/server/responses/request-prepare.ts +70 -0
  214. package/src/server/responses/request-send-budget.ts +84 -7
  215. package/src/server/responses/request-sidecar-auth.ts +16 -8
  216. package/src/server/responses/request-spend.ts +38 -9
  217. package/src/server/responses/request-transport.ts +13 -10
  218. package/src/server/responses/run-turn-execution.ts +20 -5
  219. package/src/server/responses/sidecar-execution.ts +2 -0
  220. package/src/server/responses/ws-upstream.ts +23 -2
  221. package/src/server/responses-custom-tool-repair.ts +2 -2
  222. package/src/server/sse-frame-buffer.ts +12 -10
  223. package/src/server/sse-payload-rewrite.ts +36 -9
  224. package/src/server/stop-teardown.ts +8 -1
  225. package/src/server/system-env-shell.ts +5 -1
  226. package/src/server/system-env.ts +7 -1
  227. package/src/server/workflow-refusal.ts +56 -2
  228. package/src/server/ws-bridge.ts +16 -1
  229. package/src/service/cli.ts +29 -7
  230. package/src/service/guards.ts +10 -0
  231. package/src/service/health.ts +43 -0
  232. package/src/service/state.ts +7 -2
  233. package/src/types/accounts.ts +4 -0
  234. package/src/types/config.ts +104 -3
  235. package/src/types/provider.ts +32 -0
  236. package/src/types/request.ts +7 -1
  237. package/src/types/wire.ts +9 -1
  238. package/src/usage/expected-prices.ts +28 -0
  239. package/src/usage/log.ts +87 -4
  240. package/src/web-search/passthrough-bridge.ts +39 -5
  241. package/gui/dist/assets/index-Cz7CLdif.js +0 -128
@@ -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,
@@ -10,6 +11,7 @@ import type { WsData } from "../ws-bridge";
10
11
  import { waitForProviderRequestSlot } from "../../providers/request-pacing";
11
12
  import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
12
13
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
14
+ import { configuredOutboundFetch } from "../../lib/proxy-env";
13
15
 
14
16
  export { withUpstreamHttpVersion };
15
17
 
@@ -43,6 +45,31 @@ export function safeOriginLabel(url: string): string {
43
45
  }
44
46
  }
45
47
 
48
+ /**
49
+ * Check whether a target host should bypass Bun's keep-alive pool reuse.
50
+ * Configured via the `OCX_FRESH_CONNECTION_HOSTS` environment variable (comma-separated).
51
+ */
52
+ export function wantsFreshConnection(
53
+ input: Parameters<typeof globalThis.fetch>[0],
54
+ hostsEnv = process.env.OCX_FRESH_CONNECTION_HOSTS,
55
+ ): boolean {
56
+ if (!hostsEnv) return false;
57
+ try {
58
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
59
+ const host = new URL(rawUrl).hostname.toLowerCase();
60
+ const targets = hostsEnv
61
+ .split(",")
62
+ .map(h => h.trim().toLowerCase().replace(/^\.+/, ""))
63
+ .filter(Boolean);
64
+ for (const target of targets) {
65
+ if (host === target || host.endsWith(`.${target}`)) return true;
66
+ }
67
+ } catch {
68
+ /* unparseable target URL keeps default connection behavior */
69
+ }
70
+ return false;
71
+ }
72
+
46
73
 
47
74
 
48
75
  export interface PaceAwareFetch {
@@ -52,7 +79,40 @@ export interface PaceAwareFetch {
52
79
 
53
80
  export type ProviderFetch = typeof globalThis.fetch & PaceAwareFetch;
54
81
 
82
+ /**
83
+ * Apply the physical-send connection policy to whichever fetch actually performs the send.
84
+ *
85
+ * The executor `providerFetch` builds is not the only physical boundary. A `dispatchOverride`
86
+ * that revalidates credentials re-reads `route.provider.fetch` at send time -- reselection can
87
+ * install a different provider transport after this wrapper was constructed -- and then calls
88
+ * that fetch directly instead of the supplied executor. Keeping the policy inside the executor
89
+ * alone therefore left every provider-scoped transport reusing a pooled socket for a host the
90
+ * operator had named in `OCX_FRESH_CONNECTION_HOSTS` (#4992). The policy belongs around the
91
+ * selected fetch so it follows the selection rather than the construction.
92
+ *
93
+ * Idempotent on purpose: an override that hands the send back to the supplied executor passes
94
+ * through here twice, and both passes derive the same headers from the same wire URL.
95
+ */
96
+ export function sendWithConnectionPolicy(
97
+ physicalFetch: typeof globalThis.fetch,
98
+ input: Parameters<typeof globalThis.fetch>[0],
99
+ init?: RequestInit,
100
+ ): Promise<Response> {
101
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
102
+ const fresh = wantsFreshConnection(input);
103
+ if (fresh) {
104
+ headers.set("Connection", "close");
105
+ }
106
+ return physicalFetch(input, {
107
+ ...init,
108
+ headers,
109
+ redirect: "manual",
110
+ ...(fresh ? { keepalive: false } : {}),
111
+ });
112
+ }
113
+
55
114
  export interface ProviderFetchOptions {
115
+ nativeControl?: NativeResponseControl;
56
116
  providerName?: string;
57
117
  modelId?: string;
58
118
  /** One pacing slot was acquired immediately before this fetch wrapper was created. */
@@ -70,7 +130,11 @@ export function providerFetch(
70
130
  runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
71
131
  options: ProviderFetchOptions = {},
72
132
  ): ProviderFetch {
73
- const base = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
133
+ const configuredFetch = Object.assign(
134
+ (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => configuredOutboundFetch(input, init),
135
+ { preconnect: globalThis.fetch.preconnect?.bind(globalThis.fetch) },
136
+ ) as typeof globalThis.fetch;
137
+ const base = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? configuredFetch;
74
138
  const preconnect = (...args: Parameters<typeof globalThis.fetch.preconnect>): void => {
75
139
  base.preconnect?.(...args);
76
140
  };
@@ -78,11 +142,15 @@ export function providerFetch(
78
142
  // Return the original 3xx so the response owner retains its retry/health/relay contract.
79
143
  const dispatch = Object.assign(
80
144
  (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
81
- base(input, { ...init, redirect: "manual" }),
145
+ sendWithConnectionPolicy(base, input, init),
82
146
  { preconnect },
83
147
  ) as typeof globalThis.fetch;
84
148
  const httpFetch = Object.assign(
85
149
  async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
150
+ // The hook inspects the outgoing headers and refuses the send by throwing; it is not a
151
+ // mutator, and the copy it receives is deliberately not threaded onward. `Connection`
152
+ // is decided inside `dispatch`, which runs after this, so the fresh-connection policy
153
+ // wins regardless of what any caller or hook put in the header.
86
154
  options.beforeDispatch?.(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)));
87
155
  const dispatchInit = { ...withUpstreamHttpVersion(input, init, provider), timeout: 0 };
88
156
  return options.dispatchOverride
@@ -101,7 +169,8 @@ export function providerFetch(
101
169
  // used, protocol pin included: a WS turn that falls back is serving the
102
170
  // request over HTTP, and dropping the provider's `upstreamHttpVersion`
103
171
  // there would silently negotiate a transport the operator ruled out.
104
- return codexWsUpstreamFetch(input, init, httpFetch, runtime, options.onCodexWsQuota, options.beforeDispatch);
172
+ return codexWsUpstreamFetch(input, init, httpFetch, runtime, options.onCodexWsQuota, options.beforeDispatch, options.nativeControl,
173
+ () => waitForPacing(init.signal ?? undefined));
105
174
  }
106
175
  return httpFetch(input, init);
107
176
  };
@@ -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
+ }
@@ -0,0 +1,242 @@
1
+ import { CodexWsCorrelation } from "./codex-ws-correlation";
2
+ import type { NativeResponseControl } from "./native-response-control";
3
+ import type { NativeSteeringReplayObserver } from "./native-steering-replay";
4
+ import {
5
+ injectionError, injectionFingerprint, injectionId, injectionRecord as record, injectionResults,
6
+ isInjectionRequest, MAX_NATIVE_INJECTIONS, MAX_NATIVE_INJECTION_BYTES, MAX_NATIVE_INJECTION_CALLS,
7
+ NATIVE_INJECTION_ACK_MS, NATIVE_INJECTION_TOOL_MS,
8
+ type FunctionResult, type InjectionFrame as Frame,
9
+ } from "./native-injection-protocol";
10
+
11
+ import { nativeResultKey, nativeResultMatches, nativeResultFingerprint, nativeSavedResults, nativeToolRequirement,
12
+ type NativeToolRequirement } from "./native-tool-results";
13
+
14
+ type Call = { requirement: NativeToolRequirement; state: "available" | "queued" | "accepted" | "failed"; result?: string; recoverable?: boolean };
15
+ type Submission = { frame: Frame; results: FunctionResult[]; bytes: number };
16
+ const ENVELOPE = new Set(["type", "input", "previous_response_id", "stream", "stream_id"]);
17
+
18
+ /**
19
+ * Injection-only multi-agent owner. Exactly one injection is on the physical wire
20
+ * at a time because created acknowledgements contain a response ID, not a request
21
+ * ID. Queued submissions, terminal delivery and caller-owned recovery are distinct.
22
+ */
23
+ export class NativeInjectionChannel implements NativeResponseControl {
24
+ readonly kind = "injection" as const;
25
+ relayActive = false;
26
+ replayFactory?: () => NativeSteeringReplayObserver;
27
+ private replay?: NativeSteeringReplayObserver;
28
+ private send?: (frame: Frame) => void;
29
+ private onFailure?: (error: Error) => void;
30
+ private currentId?: string;
31
+ private correlation?: CodexWsCorrelation;
32
+ private terminal?: Frame;
33
+ private terminalRecorded = false;
34
+ private continuationSent = false;
35
+ private finished = false;
36
+ private everAttached = false;
37
+ private readonly seen = new Set<string>();
38
+ private readonly calls = new Map<string, Call>();
39
+ private callBytes = 0;
40
+ private readonly queue: Submission[] = [];
41
+ private queueBytes = 0;
42
+ private inFlight?: Submission;
43
+ private lastAckSequence = -1;
44
+ private ackTimer?: ReturnType<typeof setTimeout>;
45
+ private idleTimer?: ReturnType<typeof setTimeout>;
46
+ private readonly settings = new Map<string, string>();
47
+ private readonly lane: unknown;
48
+
49
+ /** Pin the original settings and lane; construction never opens a connection. */
50
+ constructor(initial: Frame, private readonly idleMs = 300_000,
51
+ private readonly deadlines = { ackMs: NATIVE_INJECTION_ACK_MS, toolMs: NATIVE_INJECTION_TOOL_MS }) {
52
+ if (!isInjectionRequest(initial)) injectionError("injection_not_supported", "Native injection requires explicit multi_agent.enabled.");
53
+ this.lane = initial.stream_id ?? undefined;
54
+ for (const [key, value] of Object.entries(initial)) if (!ENVELOPE.has(key)) this.settings.set(key, injectionFingerprint(value));
55
+ }
56
+ /** Report that the real dispatch boundary has selected this owner. */
57
+ get attached(): boolean { return this.everAttached; }
58
+ /** A terminal is not final until submitted results have acknowledgements. */
59
+ get ended(): boolean { return this.finished; }
60
+
61
+ /** Attach once, after routing/auth/admission, retaining no global response-ID lookup. */
62
+ attach(send: (frame: Frame) => void, fail: (error: Error) => void): () => void {
63
+ if (this.everAttached) throw new Error("Native injection transport is already owned.");
64
+ this.replay = this.replayFactory?.();
65
+ this.send = send; this.onFailure = fail; this.everAttached = true;
66
+ return () => {
67
+ if (this.send !== send) return;
68
+ this.send = undefined; this.onFailure = undefined; this.finished = true;
69
+ clearTimeout(this.ackTimer); clearTimeout(this.idleTimer);
70
+ this.correlation?.finish(); this.calls.clear(); this.seen.clear(); this.queue.length = 0;
71
+ this.inFlight = undefined; this.queueBytes = 0; this.callBytes = 0; this.terminal = undefined;
72
+ this.replay?.dispose(); this.replay = undefined;
73
+ };
74
+ }
75
+ /** Never reinterpret user steering as a function result, or silently mix beta modes. */
76
+ steer(_frame: Frame): never {
77
+ return injectionError("native_control_mode_mismatch", "This multi-agent turn owns an injection-only channel; start a separate turn for steering.");
78
+ }
79
+ /** Abort unknown-delivery state without HTTP fallback, resends or invented acceptance. */
80
+ private fail(): void {
81
+ this.finished = true;
82
+ 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."));
84
+ }
85
+ /** Require the same live owner; an unbound or detached channel cannot authorize a send. */
86
+ private live(): void {
87
+ if (!this.send || this.finished) injectionError("injection_not_supported", "No live native injection transport is available on this route.");
88
+ }
89
+ /** Advertise client-owned function/custom calls and approvals, never hosted execution. */
90
+ private advertise(item: unknown): void {
91
+ const requirement = nativeToolRequirement(item);
92
+ if (!requirement) return;
93
+ const old = this.calls.get(requirement.key);
94
+ if (old) {
95
+ if (old.requirement.identity !== requirement.identity) throw new Error("Native result call identity was reused.");
96
+ return;
97
+ }
98
+ const bytes = Buffer.byteLength(JSON.stringify(requirement));
99
+ if (this.calls.size >= MAX_NATIVE_INJECTION_CALLS || this.callBytes + bytes > 256 * 1024) throw new Error("Native injection call budget exceeded.");
100
+ this.calls.set(requirement.key, { requirement, state: "available" }); this.callBytes += bytes;
101
+ }
102
+ /** Queue validated saved results; reserve each call before any possibly synchronous send. */
103
+ inject(frame: Frame): void {
104
+ this.live();
105
+ if (frame.type !== "response.inject" || !injectionId(frame.response_id)
106
+ || Object.keys(frame).some(key => !["type", "response_id", "input", "stream_id"].includes(key))) {
107
+ injectionError("invalid_injection", "Invalid native injection envelope.");
108
+ }
109
+ if (frame.response_id !== this.currentId || (frame.stream_id ?? undefined) !== this.lane || this.continuationSent) {
110
+ injectionError("injection_response_mismatch", "Injection must target the current response and lane on this connection.");
111
+ }
112
+ const results = injectionResults(frame.input);
113
+ for (const item of results) {
114
+ const call = this.calls.get(nativeResultKey(item));
115
+ if (!call || !nativeResultMatches(item, call.requirement)) injectionError("injection_call_not_found", "The result does not match a completed function call on this connection.");
116
+ if (call.state !== "available") injectionError("duplicate_injection", "This function result was already submitted; do not replay it.");
117
+ }
118
+ const text = JSON.stringify(frame);
119
+ const bytes = Buffer.byteLength(text);
120
+ if (this.queue.length >= MAX_NATIVE_INJECTIONS || bytes + this.queueBytes > MAX_NATIVE_INJECTION_BYTES) {
121
+ injectionError("injection_queue_full", "Native injection queue count or byte limit reached; no result was sent.");
122
+ }
123
+ // Detach from caller-owned objects before keeping data across asynchronous callbacks.
124
+ const copy = JSON.parse(text) as Frame;
125
+ const submission = { frame: copy, results: copy.input as FunctionResult[], bytes };
126
+ for (const item of results) this.calls.get(nativeResultKey(item))!.state = "queued";
127
+ this.queue.push(submission); this.queueBytes += bytes;
128
+ this.pump();
129
+ }
130
+ /** Dispatch one queued frame; unrelated output cannot reset its acknowledgement deadline. */
131
+ private pump(): void {
132
+ if (this.inFlight || !this.queue.length || this.finished) return;
133
+ this.live();
134
+ const submission = this.queue[0];
135
+ this.inFlight = submission;
136
+ this.ackTimer = setTimeout(() => this.fail(), this.deadlines.ackMs);
137
+ this.ackTimer.unref?.();
138
+ try {
139
+ this.replay?.submitted(submission.frame);
140
+ this.send!(submission.frame);
141
+ } catch {
142
+ this.fail();
143
+ injectionError("injection_delivery_unknown", "Injection dispatch failed; do not automatically resend or rerun tools.");
144
+ }
145
+ }
146
+ /** Match an acknowledgement to the sole in-flight frame, before releasing the next send. */
147
+ private acknowledge(event: Frame): void {
148
+ const pending = this.inFlight;
149
+ if (!pending || event.response_id !== this.currentId || !Number.isSafeInteger(event.sequence_number)
150
+ || (event.sequence_number as number) <= this.lastAckSequence) throw new Error("Native injection acknowledgement identity mismatch.");
151
+ const failed = event.type === "response.inject.failed";
152
+ if (failed && (!record(event.error) || typeof event.error.code !== "string"
153
+ || injectionFingerprint(event.input) !== injectionFingerprint(pending.results))) throw new Error("Native injection rejection does not match submitted results.");
154
+ this.replay?.observe(event);
155
+ this.lastAckSequence = event.sequence_number as number;
156
+ for (const item of pending.results) {
157
+ const call = this.calls.get(nativeResultKey(item))!;
158
+ call.state = failed ? "failed" : "accepted";
159
+ // Retain a digest, not another result body, for an explicitly rejected continuation.
160
+ call.recoverable = failed && record(event.error) && event.error.code === "response_already_completed";
161
+ if (call.recoverable) call.result = nativeResultFingerprint(item);
162
+ }
163
+ clearTimeout(this.ackTimer); this.ackTimer = undefined;
164
+ this.inFlight = undefined; this.queue.shift(); this.queueBytes -= pending.bytes;
165
+ // 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(); } });
167
+ }
168
+ /** Commit terminal replay only when no submitted injection can change its accepted inputs. */
169
+ private recordTerminal(): void {
170
+ if (this.terminal && !this.terminalRecorded) { this.replay?.observe(this.terminal); this.terminalRecorded = true; }
171
+ }
172
+ /** Recover saved, explicitly rejected results on the same socket only when the client asks. */
173
+ continue(frame: Frame): boolean {
174
+ if (!this.send || this.finished) return false;
175
+ if (this.queue.length || this.continuationSent) injectionError("injection_pending", "Wait for every injection acknowledgement before creating another response.");
176
+ if (!this.terminal && frame.previous_response_id === this.currentId) {
177
+ injectionError("injection_pending", "Wait for the response terminal before sending saved-result continuations.");
178
+ }
179
+ if (!this.terminal || frame.previous_response_id !== this.currentId) return false;
180
+ if (this.terminal.type !== "response.completed") injectionError("injection_response_failed", "The parent response did not complete successfully.");
181
+ if ((frame.stream_id ?? undefined) !== this.lane || frame.generate === false) injectionError("invalid_injection", "Use the same lane for an injection continuation.");
182
+ for (const [key, value] of Object.entries(frame)) {
183
+ if (!ENVELOPE.has(key) && this.settings.get(key) !== injectionFingerprint(value)) injectionError("injection_settings_changed", "A native injection continuation cannot change the pinned model or settings.");
184
+ }
185
+ for (const key of this.settings.keys()) {
186
+ if (!Object.hasOwn(frame, key)) injectionError("injection_settings_changed", "A native injection continuation cannot change the pinned model or settings.");
187
+ }
188
+ const results = nativeSavedResults(frame.input);
189
+ const required = [...this.calls.entries()].filter(([, call]) => call.state !== "accepted");
190
+ if (!required.length || results.length !== required.length) injectionError("invalid_injection", "Supply every outstanding saved tool result exactly once.");
191
+ for (const item of results) {
192
+ const call = this.calls.get(nativeResultKey(item));
193
+ if (!call || !nativeResultMatches(item, call.requirement) || call.state === "accepted" || call.state === "queued"
194
+ || (call.state === "failed" && (!call.recoverable || call.result !== nativeResultFingerprint(item)))) {
195
+ injectionError("invalid_injection", "Continuation input must match unsent or explicitly completion-rejected tool results.");
196
+ }
197
+ }
198
+ if (Buffer.byteLength(JSON.stringify(frame)) > MAX_NATIVE_INJECTION_BYTES) injectionError("invalid_injection", "Native injection continuation exceeds its byte limit.");
199
+ 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."); }
202
+ if (!this.finished) this.armIdle(this.deadlines.ackMs);
203
+ return true;
204
+ }
205
+ /** Manage response/tool liveness independently of the non-resettable acknowledgement timer. */
206
+ private armIdle(ms: number): void {
207
+ clearTimeout(this.idleTimer);
208
+ this.idleTimer = setTimeout(() => this.fail(), ms); this.idleTimer.unref?.();
209
+ }
210
+ /** Validate ordered upstream events while allowing acknowledgements after a response terminal. */
211
+ observe(event: Frame): boolean {
212
+ if ((event.stream_id ?? undefined) !== this.lane && !(event.type === "error" && event.stream_id == null)) throw new Error("Native injection lane mismatch.");
213
+ const type = event.type;
214
+ if (type === "error") { this.finished = true; return true; }
215
+ if (type === "response.inject.created" || type === "response.inject.failed") this.acknowledge(event);
216
+ else {
217
+ if (typeof type === "string" && (type.startsWith("response.inject.") || type.startsWith("response.steer."))) throw new Error("Unsupported native injection control event.");
218
+ const response = record(event.response) ? event.response : undefined;
219
+ if (type === "response.created") {
220
+ if (!injectionId(response?.id) || this.seen.has(response.id) || this.seen.size >= 128) throw new Error("Native injection response identity or chain limit violated.");
221
+ if (this.currentId && (!this.continuationSent || !this.terminal || this.queue.length
222
+ || (response.previous_response_id != null && response.previous_response_id !== this.currentId))) throw new Error("Unexpected native injection successor.");
223
+ this.currentId = response.id; this.seen.add(response.id); this.calls.clear(); this.callBytes = 0;
224
+ this.terminal = undefined; this.terminalRecorded = false; this.continuationSent = false; this.lastAckSequence = -1;
225
+ this.correlation?.finish(); this.correlation = new CodexWsCorrelation(true, () => false);
226
+ } else if (!this.currentId || this.terminal) throw new Error("Unexpected native injection event outside an active response.");
227
+ this.correlation?.accept({ ...event, stream_id: undefined });
228
+ if (type === "response.output_item.done") this.advertise(event.item);
229
+ if (["response.completed", "response.failed", "response.incomplete"].includes(String(type))) {
230
+ if (!this.currentId || response?.id !== this.currentId) throw new Error("Native injection terminal identity mismatch.");
231
+ this.terminal = event;
232
+ if (Array.isArray(response.output)) for (const item of response.output) this.advertise(item);
233
+ } else this.replay?.observe(event);
234
+ }
235
+ const unresolved = [...this.calls.values()].some(call => call.state !== "accepted");
236
+ this.finished = Boolean(this.terminal && !this.queue.length && !this.continuationSent
237
+ && (this.terminal.type !== "response.completed" || !unresolved));
238
+ if (this.finished) { this.recordTerminal(); clearTimeout(this.idleTimer); }
239
+ else this.armIdle(this.continuationSent ? this.deadlines.ackMs : unresolved ? this.deadlines.toolMs : this.idleMs);
240
+ return this.finished;
241
+ }
242
+ }
@@ -0,0 +1,56 @@
1
+ import type { OcxProviderConfig } from "../../types";
2
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
3
+ import type { NativeSteeringReplayObserver } from "./native-steering-replay";
4
+
5
+ import { isInjectionRequest } from "./native-injection-protocol";
6
+
7
+ /** Shared transport ownership, not a shared steer/inject protocol state machine. */
8
+ export interface NativeResponseControl {
9
+ readonly kind?: "steering" | "injection";
10
+ relayActive: boolean;
11
+ normalizeContinuation?: (frame: Record<string, unknown>) => Record<string, unknown>;
12
+ replayFactory?: () => NativeSteeringReplayObserver;
13
+ readonly attached: boolean;
14
+ readonly ended: boolean;
15
+ attach(send: (frame: Record<string, unknown>) => void, fail: (error: Error) => void): () => void;
16
+ observe(frame: Record<string, unknown>): boolean;
17
+ steer(frame: Record<string, unknown>): void;
18
+ inject?(frame: Record<string, unknown>): void;
19
+ continue(frame: Record<string, unknown>): boolean;
20
+ }
21
+
22
+ export const OPENAI_API_RESPONSES_URL = "https://api.openai.com/v1/responses";
23
+
24
+ const nativeControlResponses = new WeakSet<Response>();
25
+ /** Mark the exact response for multi-response delivery without serializing a wire field. */
26
+ export function markNativeControlResponse(response: Response): Response { nativeControlResponses.add(response); return response; }
27
+ /** Recognize a marked native response by identity, not by caller-controlled content. */
28
+ export function isNativeControlResponse(response: Response): boolean { return nativeControlResponses.has(response); }
29
+
30
+ /** Preserve canonical ChatGPT eligibility; public API controls require an explicit provider WebSocket opt-in. */
31
+ export function nativeResponseControlEligible(provider: OcxProviderConfig, control?: NativeResponseControl): boolean {
32
+ if (isCanonicalOpenAiForwardProvider(provider)) return true;
33
+ return (control?.kind === "injection" || control?.kind === "steering") && provider.adapter === "openai-responses"
34
+ && provider.upstreamWebsocket === true && provider.authMode !== "forward"
35
+ && provider.baseUrl?.replace(/\/+$/, "") === "https://api.openai.com/v1";
36
+ }
37
+
38
+ /** Select by execution mode, never model name; a multi-agent request cannot acquire steering. */
39
+ export function nativeResponseControlMode(frame: Record<string, unknown>, flags: {
40
+ codexNativeInjection?: boolean; codexNativeSteering?: boolean;
41
+ }): "injection" | "steering" | undefined {
42
+ if (isInjectionRequest(frame)) return flags.codexNativeInjection === true ? "injection" : undefined;
43
+ return nativeSteeringUnavailableReason(frame, flags.codexNativeSteering) === undefined ? "steering" : undefined;
44
+ }
45
+
46
+ /** Explain documented execution-mode exclusions without claiming model entitlement. */
47
+ export function nativeSteeringUnavailableReason(frame: Record<string, unknown>, enabled?: boolean): string | undefined {
48
+ if (enabled !== true) return "Native steering is disabled; enable codexNativeSteering and WebSockets for a supported route.";
49
+ if (isInjectionRequest(frame)) return "Multi-agent execution does not support single-agent response.steer; use a later client request.";
50
+ if (frame.conversation != null) return "Conversation-bound responses do not support native steering.";
51
+ if (Array.isArray(frame.context_management) && frame.context_management.some(item =>
52
+ item && typeof item === "object" && (item as Record<string, unknown>).type === "compaction")) {
53
+ return "Automatic API compaction and native steering cannot share an active response.";
54
+ }
55
+ return undefined;
56
+ }
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /** Narrow JSON object envelopes independently of either native control owner. */
4
+ export function nativeResponseRecord(value: unknown): value is Record<string, unknown> {
5
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+
8
+ /** Compare JSON content by value: object-key order is irrelevant, array order is not. */
9
+ export function nativeResponseFingerprint(value: unknown): string {
10
+ const canonical = (item: unknown): string => Array.isArray(item) ? `[${item.map(canonical).join(",")}]`
11
+ : nativeResponseRecord(item) ? `{${Object.keys(item).sort().map(key => `${JSON.stringify(key)}:${canonical(item[key])}`).join(",")}}`
12
+ : JSON.stringify(item) ?? "null";
13
+ return createHash("sha256").update(canonical(value)).digest("hex");
14
+ }
@@ -0,0 +1,37 @@
1
+ import { nativeResponseFingerprint as injectionFingerprint, nativeResponseRecord as record } from "./native-response-json";
2
+ type Frame = Record<string, unknown>;
3
+
4
+ /**
5
+ * Preserve completed wire items missing from a sparse terminal, including hosted
6
+ * tool results and encrypted agent messages. Shared IDs must retain content and
7
+ * relative order; conflicting transcripts fail instead of silently losing data.
8
+ */
9
+ export function nativeResponseOutput(done: ReadonlyMap<number, Frame>, terminal: unknown): Frame[] {
10
+ const observed = [...done.entries()].sort((a, b) => a[0] - b[0]).map(([, item]) => item);
11
+ if (terminal == null || (Array.isArray(terminal) && !terminal.length)) return observed;
12
+ if (!Array.isArray(terminal) || terminal.some(item => !record(item))) throw new Error("Invalid native response output.");
13
+ const identity = (item: Frame) => typeof item.id === "string" ? `id:${item.id}` : `body:${injectionFingerprint(item)}`;
14
+ const positions = new Map<string, number>();
15
+ observed.forEach((item, index) => {
16
+ const key = identity(item);
17
+ if (positions.has(key)) throw new Error("Duplicate native completed output identity.");
18
+ positions.set(key, index);
19
+ });
20
+ const result: Frame[] = [];
21
+ const seen = new Set<string>();
22
+ let cursor = 0;
23
+ for (const item of terminal as Frame[]) {
24
+ const key = identity(item);
25
+ if (seen.has(key)) throw new Error("Duplicate native terminal output identity.");
26
+ seen.add(key);
27
+ const position = positions.get(key);
28
+ if (position === undefined) { result.push(item); continue; }
29
+ if (position < cursor || injectionFingerprint(item) !== injectionFingerprint(observed[position])) {
30
+ throw new Error("Native terminal output contradicts completed wire items.");
31
+ }
32
+ while (cursor < position) result.push(observed[cursor++]);
33
+ result.push(item); cursor++;
34
+ }
35
+ while (cursor < observed.length) result.push(observed[cursor++]);
36
+ return result;
37
+ }