@bitkyc08/opencodex 2.57.0 → 2.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/gui/dist/assets/{index-Cz7CLdif.js → index-BbrHOIY0.js} +2 -2
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -2
  4. package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
  5. package/src/adapters/command-code.ts +13 -4
  6. package/src/adapters/cursor/cursor-errors.ts +15 -0
  7. package/src/adapters/cursor/discovery.ts +65 -1
  8. package/src/adapters/cursor/live-transport.ts +5 -1
  9. package/src/adapters/cursor/protobuf-events.ts +110 -11
  10. package/src/adapters/cursor/protobuf-request.ts +19 -1
  11. package/src/adapters/cursor/text-toolcall.ts +230 -0
  12. package/src/adapters/cursor/thread-continuity.ts +67 -0
  13. package/src/adapters/cursor/types.ts +5 -0
  14. package/src/adapters/cursor.ts +55 -5
  15. package/src/adapters/google-http.ts +38 -13
  16. package/src/adapters/mimo-free.ts +32 -17
  17. package/src/adapters/ollama-native.ts +42 -8
  18. package/src/adapters/openai-responses/passthrough.ts +30 -4
  19. package/src/adapters/openai-responses/request-strips.ts +43 -0
  20. package/src/adapters/physical-send.ts +50 -0
  21. package/src/bridge/response-json.ts +1 -1
  22. package/src/bridge/sse.ts +1 -1
  23. package/src/claude/outbound.ts +14 -4
  24. package/src/cli/config-command.ts +35 -18
  25. package/src/cli/dispatch.ts +17 -4
  26. package/src/cli/index.ts +44 -2
  27. package/src/cli/system-command.ts +70 -1
  28. package/src/cli/uninstall-client-state.ts +12 -0
  29. package/src/codex/auth-context.ts +42 -8
  30. package/src/codex/desktop-switches.ts +145 -0
  31. package/src/codex/history-job.ts +5 -1
  32. package/src/codex/history-provider.ts +33 -4
  33. package/src/codex/history-worker.ts +14 -1
  34. package/src/codex/inject/remove.ts +145 -7
  35. package/src/codex/inject/restore.ts +204 -32
  36. package/src/codex/inject.ts +3 -7
  37. package/src/codex/loopback-target.ts +9 -0
  38. package/src/codex/native-profile-startup.ts +64 -20
  39. package/src/config/atomic-write.ts +83 -8
  40. package/src/config/schema/config-schema.ts +2 -0
  41. package/src/config/schema/leaf-validators.ts +1 -0
  42. package/src/generated/compatibility-version.json +132 -68
  43. package/src/lib/bounded-subprocess.ts +62 -10
  44. package/src/lib/windows-secret-acl.ts +151 -15
  45. package/src/lib/windows-user-principal.ts +5 -1
  46. package/src/providers/derive.ts +6 -0
  47. package/src/providers/model-discovery.ts +19 -7
  48. package/src/providers/registry/entries-core.ts +11 -0
  49. package/src/providers/registry/entries-extended.ts +50 -28
  50. package/src/providers/registry/model-seeds.ts +67 -17
  51. package/src/providers/registry/types.ts +9 -0
  52. package/src/responses/spill-store.ts +17 -0
  53. package/src/responses/state/body-policy.ts +25 -0
  54. package/src/responses/state/spill-queue.ts +8 -6
  55. package/src/responses/state.ts +3 -22
  56. package/src/router.ts +4 -0
  57. package/src/server/auth-cors.ts +1 -0
  58. package/src/server/index/websocket-handler.ts +48 -1
  59. package/src/server/management/config-routes.ts +27 -5
  60. package/src/server/models-capabilities.ts +24 -3
  61. package/src/server/responses/codex-ws-exchange.ts +65 -4
  62. package/src/server/responses/combo-stream-preflight.ts +68 -5
  63. package/src/server/responses/core-combo.ts +26 -0
  64. package/src/server/responses/core-options.ts +3 -0
  65. package/src/server/responses/fetch-helpers.ts +4 -1
  66. package/src/server/responses/native-injection-protocol.ts +42 -0
  67. package/src/server/responses/native-injection-replay.ts +105 -0
  68. package/src/server/responses/native-injection.ts +242 -0
  69. package/src/server/responses/native-response-control.ts +56 -0
  70. package/src/server/responses/native-response-json.ts +14 -0
  71. package/src/server/responses/native-response-output.ts +37 -0
  72. package/src/server/responses/native-steering-log.ts +44 -0
  73. package/src/server/responses/native-steering-policy.ts +49 -0
  74. package/src/server/responses/native-steering-replay.ts +126 -0
  75. package/src/server/responses/native-steering-settings.ts +76 -0
  76. package/src/server/responses/native-steering.ts +400 -0
  77. package/src/server/responses/native-tool-results.ts +130 -0
  78. package/src/server/responses/passthrough-delivery.ts +11 -0
  79. package/src/server/responses/passthrough-dispatch.ts +33 -1
  80. package/src/server/responses/request-prepare.ts +41 -0
  81. package/src/server/responses/ws-upstream.ts +21 -1
  82. package/src/server/stop-teardown.ts +8 -1
  83. package/src/server/ws-bridge.ts +16 -1
  84. package/src/service/cli.ts +13 -1
  85. package/src/types/config.ts +4 -0
  86. package/src/types/provider.ts +13 -0
@@ -36,6 +36,7 @@ import {
36
36
  previewCodexPoolLineage,
37
37
  applyCodexAuthContextToProvider,
38
38
  hasCallerCodexBearer,
39
+ requestOwnedMainPinState,
39
40
  } from "../../codex/auth-context";
40
41
  import {
41
42
  copyPreviousResponseReplayProvenance,
@@ -483,11 +484,37 @@ export async function prepareResponsesRequest(
483
484
  const nativeMainReadsForbidden = previewRequestScopedMainCredential
484
485
  || nativeMainRecoveryBlocked
485
486
  || previewSelectionAdmission?.mainProfileDraining === true;
487
+ // The liveness answer final authentication gives its own selection options, computed from the
488
+ // same shared predicate so the two cannot drift apart again (#4850). `fixedAccountId` is
489
+ // mirrored through `route.codexAccountId` because that is literally what core-auth.ts passes
490
+ // as `accountId`. A reserve-authorized request is the one input where the two can differ, and
491
+ // it differs harmlessly: reserve plus a caller bearer is served as main either way, which is
492
+ // the answer this produces.
493
+ const previewRequestOwnedMainPin = requestOwnedMainPinState(
494
+ previewAuthHeaders,
495
+ config,
496
+ options.codexAuthPolicy ?? config,
497
+ previewRequestScopedMainCredential,
498
+ route.codexAccountId,
499
+ ).preserve;
486
500
  // Deliberately NOT fenced on ownership: final auth derives `nativeMainSelectionOnly` from the
487
501
  // drain alone, and adding a term here would diverge from it in the other direction.
488
502
  const previewSelectionOptions = {
489
503
  nativeMainSelectionOnly: !nativeMainRecoveryBlocked
490
504
  && previewSelectionAdmission?.mainProfileDraining === true,
505
+ // Pool eligibility was the last part of preview still outside the fence (#4850). Without
506
+ // this seam `codexAccountUnusableReason` takes its default branch into
507
+ // `isMainAccountCredentialUsable()`, which opens the physical `auth.json` -- twice per
508
+ // spawn, because subagent fallback re-enters the preview through the callback below.
509
+ //
510
+ // Scoped to ownership, and carrying final auth's value rather than a constant, because
511
+ // preview exists to predict final auth. Under an effective main pin the request really is
512
+ // served by its own main credential, so main must stay eligible; without the pin final auth
513
+ // scores main `main_credential_unavailable` and drops it, so preview has to drop it too. A
514
+ // hardcoded `true` would be wrong in the second case and `false` in the first.
515
+ isMainAccountTokenLive: previewRequestScopedMainCredential
516
+ ? () => previewRequestOwnedMainPin
517
+ : undefined,
491
518
  // Preview must reach the same answer as the final resolution, including the uploaded-file
492
519
  // retention (#4778): a preview that reported a quota move the request will not make would
493
520
  // hand subagent fallback a different account than the one that actually serves.
@@ -711,9 +738,23 @@ export async function prepareResponsesRequest(
711
738
  route,
712
739
  options,
713
740
  ).requestScopedMainCredential && hasCallerCodexBearer(recoveryAuthHeaders);
741
+ // Recovery's own answer to the same question, against the route it may have moved
742
+ // to. Reconstructing the options without it is what left pool eligibility outside
743
+ // the fence on the first preview (#4850); recovery re-previews, so it would leave
744
+ // the same two reads on the one path that runs after decryption.
745
+ const recoveryRequestOwnedMainPin = requestOwnedMainPinState(
746
+ recoveryAuthHeaders,
747
+ config,
748
+ options.codexAuthPolicy ?? config,
749
+ recoveryRequestScopedMainCredential,
750
+ route.codexAccountId,
751
+ ).preserve;
714
752
  const recoverySelectionOptions = {
715
753
  nativeMainSelectionOnly: !recoveryNativeMainBlocked
716
754
  && recoverySelectionAdmission?.mainProfileDraining === true,
755
+ isMainAccountTokenLive: recoveryRequestScopedMainCredential
756
+ ? () => recoveryRequestOwnedMainPin
757
+ : undefined,
717
758
  // #4778, same reason as `previewSelectionOptions` above: this preview decides
718
759
  // which account subagent fallback scores against, and final auth passes the
719
760
  // retention. Recovery is exactly where the two could diverge -- it re-previews
@@ -1,3 +1,6 @@
1
+ import { OPENAI_API_RESPONSES_URL } from "./native-response-control";
2
+ import { isInjectionRequest } from "./native-injection-protocol";
3
+ import type { NativeResponseControl } from "./native-response-control";
1
4
  // Upstream WebSocket transport for the ChatGPT Codex backend.
2
5
  //
3
6
  // Why this exists: the Codex backend serves the responses_websockets path from
@@ -129,6 +132,8 @@ export function codexWsUpstreamFetch(
129
132
  runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
130
133
  onQuota?: CodexWsQuotaObserver,
131
134
  beforeDispatch?: (headers: Headers) => void,
135
+ nativeControl?: NativeResponseControl,
136
+ beforeContinuation?: () => Promise<void>,
132
137
  ): Promise<Response> {
133
138
  const prepared = prepareCodexWsRequest(url, init);
134
139
  if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
@@ -142,6 +147,17 @@ export function codexWsUpstreamFetch(
142
147
  }
143
148
 
144
149
  const { frameText, headers } = prepared;
150
+ // Never infer backend support from a model name or enable controls on a gateway.
151
+ const control = nativeControl?.kind === "injection"
152
+ ? ((prepared.canonical || url === OPENAI_API_RESPONSES_URL) && isInjectionRequest(JSON.parse(frameText)) ? nativeControl : undefined)
153
+ : (prepared.canonical || url === OPENAI_API_RESPONSES_URL) ? nativeControl : undefined;
154
+ if (control?.kind === "injection" && url === OPENAI_API_RESPONSES_URL) {
155
+ const beta = headers["openai-beta"];
156
+ if (!beta?.split(",").some(value => value.trim() === "responses_multi_agent=v1")) {
157
+ headers["openai-beta"] = beta ? `${beta}, responses_multi_agent=v1` : "responses_multi_agent=v1";
158
+ }
159
+ }
160
+
145
161
 
146
162
  // Decide before dialing. Once the socket is open the caller already holds a
147
163
  // streaming Response, so the oversized close can only be surfaced as a stream
@@ -169,7 +185,9 @@ export function codexWsUpstreamFetch(
169
185
  }
170
186
  let session: CodexWsSession;
171
187
  try {
172
- const identity = codexWsReuseIdentity(url, headers, frameText, proxy);
188
+ // Steering keeps a private physical connection across successor responses; it
189
+ // must never enter the idle-socket pool or move to a different credential.
190
+ const identity = control ? null : codexWsReuseIdentity(url, headers, frameText, proxy);
173
191
  session = (identity ? codexWsPool.acquire(identity, wsUrl, headers, proxy) : null)
174
192
  ?? new CodexWsSession(wsUrl, headers, false, undefined, proxy);
175
193
  if (!session.busy && !session.reserve()) {
@@ -181,6 +199,8 @@ export function codexWsUpstreamFetch(
181
199
  }
182
200
  return codexWsExchange({
183
201
  session, url, init, prepared, sseFallback, onQuota, beforeDispatch,
202
+ nativeControl: control,
203
+ beforeContinuation,
184
204
  bunVersion: typeof runtime === "string" ? runtime : runtime.version,
185
205
  });
186
206
  }
@@ -67,7 +67,14 @@ export async function performStopTeardown(url: URL, io: StopTeardownIo = {}): Pr
67
67
  // undone (#3008).
68
68
  const grokNote = grok.ok ? "" : ` Grok config cleanup failed: ${grok.message}`;
69
69
  if (restore.success && grok.ok) {
70
- return { success: true, message: "Proxy stopping, native Codex restored.", sharedTeardown: "performed" };
70
+ // A degraded restore is a success — routing is out and the client is no longer aimed at
71
+ // a port that is about to disappear — but it left a provider table behind on purpose.
72
+ // Reporting a bare "restored" would put the caller in exactly the position #4812
73
+ // describes: a config they did not expect and no idea why it is there.
74
+ const retained = restore.retainedCodexProviderTable
75
+ ? ` ${(await import("../codex/inject/restore")).describeRetainedCodexProviderTable(restore.retainedCodexProviderTable)}`
76
+ : "";
77
+ return { success: true, message: `Proxy stopping, native Codex restored.${retained}`, sharedTeardown: "performed" };
71
78
  }
72
79
  if (restore.success) {
73
80
  return {
@@ -1,3 +1,4 @@
1
+ import type { NativeResponseControl } from "./responses/native-response-control";
1
2
  import type { ServerWebSocket } from "bun";
2
3
  import { responsesJsonEventSequence } from "./responses-json-events";
3
4
  import { FORWARD_HEADERS } from "../adapters/openai-responses";
@@ -17,6 +18,9 @@ type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
17
18
  type ResponsesPayloadObserver = (payload: string) => void;
18
19
 
19
20
  export interface WsData {
21
+ nativeControl?: NativeResponseControl;
22
+ /** Content-free per-turn explanation; never a model capability assertion. */
23
+ nativeSteeringUnavailable?: string;
20
24
  headers?: Headers; // base inbound forward headers only; per-turn auth refresh injects current pool tokens
21
25
  /**
22
26
  * Resolved once at the handshake. Auth is handshake-time only on this path, so
@@ -229,6 +233,7 @@ export async function pumpResponsesSseToWebSocket(
229
233
  sseStream: ReadableStream<Uint8Array>,
230
234
  options: {
231
235
  isCurrent?: () => boolean;
236
+ untilEof?: boolean;
232
237
  onTerminal?: ResponsesTerminalReporter;
233
238
  onSsePayload?: ResponsesPayloadObserver;
234
239
  } = {},
@@ -251,6 +256,7 @@ export async function pumpResponsesSseToWebSocket(
251
256
  const decoder = new TextDecoder();
252
257
  const framer = new BoundedSseFrameBuffer();
253
258
  let terminalSeen = false;
259
+ let lastTerminal: ResponsesTerminalStatus | undefined;
254
260
 
255
261
  const handlePayload = (payload: string): boolean => {
256
262
  if (!isCurrent()) return true;
@@ -270,8 +276,10 @@ export async function pumpResponsesSseToWebSocket(
270
276
  }
271
277
  if (terminalSeen) return true;
272
278
  sendTextFrame(ws, payload);
273
- const terminalStatus = terminalStatusFromType(type);
279
+ if (options.untilEof && type === "response.created") lastTerminal = undefined;
280
+ const terminalStatus = type === "error" && options.untilEof ? "failed" : terminalStatusFromType(type);
274
281
  if (terminalStatus) {
282
+ if (options.untilEof) { lastTerminal = terminalStatus; return false; }
275
283
  reportTerminal(terminalStatus);
276
284
  terminalSeen = true;
277
285
  void reader.cancel().catch(() => {});
@@ -294,6 +302,10 @@ export async function pumpResponsesSseToWebSocket(
294
302
  const payload = parseSseBlock(decoder.decode(tail));
295
303
  if (payload) handlePayload(payload);
296
304
  }
305
+ if (options.untilEof && lastTerminal && isCurrent() && !clientCancelled) {
306
+ reportTerminal(lastTerminal);
307
+ terminalSeen = true;
308
+ }
297
309
  if (!terminalSeen && isCurrent() && !clientCancelled) {
298
310
  reportTerminal("incomplete");
299
311
  sendProtocolError(ws, 502, "Upstream stream ended before response terminal event");
@@ -368,6 +380,7 @@ export async function sendResponseToWebSocket(
368
380
  response: Response,
369
381
  isCurrent: () => boolean,
370
382
  options: {
383
+ untilEof?: boolean;
371
384
  onTerminal?: ResponsesTerminalReporter;
372
385
  onSsePayload?: ResponsesPayloadObserver;
373
386
  } = {},
@@ -398,6 +411,7 @@ export async function sendResponseToWebSocket(
398
411
  if (contentType.includes("text/event-stream")) {
399
412
  await pumpResponsesSseToWebSocket(ws, response.body, {
400
413
  isCurrent,
414
+ untilEof: options.untilEof,
401
415
  onTerminal: options.onTerminal,
402
416
  onSsePayload: options.onSsePayload,
403
417
  });
@@ -420,6 +434,7 @@ export async function sendResponseToWebSocket(
420
434
  if (looksLikeSse(prefix)) {
421
435
  await pumpResponsesSseToWebSocket(ws, stream, {
422
436
  isCurrent,
437
+ untilEof: options.untilEof,
423
438
  onTerminal: options.onTerminal,
424
439
  onSsePayload: options.onSsePayload,
425
440
  });
@@ -1,6 +1,7 @@
1
1
  import { existsSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { restoreNativeCodexAsync } from "../codex/inject";
4
+ import { describeRetainedCodexProviderTable } from "../codex/inject/restore";
4
5
  import { stripGrokConfig } from "../grok/inject";
5
6
  import { serviceApiTokenFilePath } from "../lib/service-secrets";
6
7
  import { statusWinswRaw, type WinswStatus } from "../lib/winsw";
@@ -291,7 +292,15 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
291
292
  break;
292
293
  }
293
294
  const restore = await restoreNativeCodexAsync();
294
- if (restore.success) console.log("✅ service stopped + native Codex restored.");
295
+ if (restore.success) {
296
+ console.log("✅ service stopped + native Codex restored.");
297
+ // Success is not the whole answer when routing came down but the provider table
298
+ // stayed. Saying only "restored" here is how a user finds an unexplained
299
+ // opencodex table in their config weeks later (#4812).
300
+ if (restore.retainedCodexProviderTable) {
301
+ console.log(` ${describeRetainedCodexProviderTable(restore.retainedCodexProviderTable)}`);
302
+ }
303
+ }
295
304
  else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
296
305
  if (!restore.success) process.exitCode = 1;
297
306
  // The Grok fence is the other managed config this command owns. Leaving it behind
@@ -339,6 +348,9 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
339
348
  console.error(`⚠️ native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` before using native Codex.`);
340
349
  process.exitCode = 1;
341
350
  }
351
+ else if (restore.retainedCodexProviderTable) {
352
+ console.log(`↩️ ${describeRetainedCodexProviderTable(restore.retainedCodexProviderTable)}`);
353
+ }
342
354
  const grok = stripGrokConfig();
343
355
  if (grok.changed) console.log(`↩️ ${grok.message}`);
344
356
  else if (!grok.ok) {
@@ -749,6 +749,10 @@ export interface OcxConfig {
749
749
  shutdownTimeoutMs?: number;
750
750
  /** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
751
751
  websockets?: boolean;
752
+ /** Experimental single-lane native OpenAI WebSocket steering; default off. */
753
+ codexNativeSteering?: boolean;
754
+ /** Experimental, default-off saved function-result injection on native multi-agent WebSockets. */
755
+ codexNativeInjection?: boolean;
752
756
  /**
753
757
  * Opt-in auto-cleanup policy for archived Codex sessions (issue #42 Phase 3).
754
758
  * Default OFF (`enabled` false / unset). Never enabled implicitly.
@@ -311,6 +311,19 @@ export interface OcxProviderConfig {
311
311
  * preserved after it, and parallel calls stay together with the reasoning turn that produced them.
312
312
  */
313
313
  requiresAdjacentResponsesToolResults?: boolean;
314
+ /**
315
+ * Responses upstream whose parser also rejects a tool call that has no matching output
316
+ * anywhere in the replayed input, not merely one whose result sits out of order. A call left
317
+ * dangling by an interrupted stream is answered with an explicit unknown-status placeholder
318
+ * so the thread can continue.
319
+ *
320
+ * Separate from `requiresAdjacentResponsesToolResults` on purpose: adjacency reorders items a
321
+ * strict parser already accepts in some order, while this synthesizes an item the client never
322
+ * sent. Kimi accepts a dangling call (#4726), so it must not inherit the synthesis.
323
+ * `statelessResponses` implies this, because an upstream that stores nothing cannot resolve
324
+ * the missing half from its own history either.
325
+ */
326
+ requiresPairedResponsesToolResults?: boolean;
314
327
  /**
315
328
  * When enabled, a tool result that is present but empty (no usable text or content
316
329
  * part) is rewritten to an explicit annotation before it reaches the upstream wire,