@bitkyc08/opencodex 2.7.36 → 2.7.37

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 (66) hide show
  1. package/README.ja.md +8 -1
  2. package/README.ko.md +7 -1
  3. package/README.md +7 -1
  4. package/README.ru.md +7 -1
  5. package/README.zh-CN.md +7 -1
  6. package/gui/dist/assets/index-BhUTxmCy.js +52 -0
  7. package/gui/dist/assets/index-oOZcqVmj.css +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic.ts +22 -2
  11. package/src/adapters/cursor/live-transport.ts +7 -0
  12. package/src/adapters/cursor/message-mapper.ts +3 -0
  13. package/src/adapters/cursor/protobuf-request.ts +223 -27
  14. package/src/adapters/cursor/request-builder.ts +41 -15
  15. package/src/adapters/cursor/thread-continuity.ts +67 -0
  16. package/src/adapters/cursor/types.ts +3 -1
  17. package/src/adapters/cursor.ts +44 -9
  18. package/src/adapters/google.ts +115 -62
  19. package/src/adapters/kiro.ts +3 -17
  20. package/src/adapters/openai-chat.ts +16 -5
  21. package/src/adapters/openai-responses.ts +56 -1
  22. package/src/adapters/run-turn-queue.ts +11 -1
  23. package/src/bridge.ts +139 -69
  24. package/src/chat/outbound.ts +135 -73
  25. package/src/cli/codex-shim-autorestore.ts +45 -0
  26. package/src/cli/doctor.ts +197 -2
  27. package/src/cli/index.ts +17 -3
  28. package/src/cli/status.ts +80 -0
  29. package/src/cli/v2.ts +14 -2
  30. package/src/codex/auth-context.ts +18 -2
  31. package/src/codex/catalog/bundled.ts +83 -27
  32. package/src/codex/catalog/effort.ts +95 -3
  33. package/src/codex/catalog/parsing.ts +17 -0
  34. package/src/codex/catalog/provider-fetch.ts +31 -8
  35. package/src/codex/exec-invocation.ts +22 -0
  36. package/src/codex/model-cache.ts +44 -0
  37. package/src/codex/runtime.ts +529 -0
  38. package/src/codex/shim.ts +608 -10
  39. package/src/combos/resolve.ts +7 -2
  40. package/src/config.ts +32 -1
  41. package/src/lib/bun-stream-caps.ts +88 -0
  42. package/src/lib/crash-guard.ts +3 -1
  43. package/src/lib/sse-decoder.ts +25 -6
  44. package/src/responses/parser.ts +2 -1
  45. package/src/responses/state.ts +10 -2
  46. package/src/server/auth-cors.ts +4 -1
  47. package/src/server/index.ts +191 -1
  48. package/src/server/live.ts +491 -0
  49. package/src/server/management/config-routes.ts +79 -3
  50. package/src/server/management/provider-routes.ts +2 -0
  51. package/src/server/management/shared.ts +6 -6
  52. package/src/server/management/system-routes.ts +65 -0
  53. package/src/server/management-api.ts +3 -1
  54. package/src/server/memory-watchdog.ts +112 -0
  55. package/src/server/relay-eager.ts +199 -0
  56. package/src/server/relay.ts +131 -81
  57. package/src/server/responses/collaboration.ts +20 -3
  58. package/src/server/responses/core.ts +236 -21
  59. package/src/server/responses/encrypted-payload.ts +118 -41
  60. package/src/server/ws-bridge.ts +7 -0
  61. package/src/types.ts +25 -0
  62. package/src/usage/cost.ts +0 -0
  63. package/src/usage/expected-prices.ts +19 -0
  64. package/src/usage/summary.ts +11 -8
  65. package/gui/dist/assets/index-BpX-hoSd.css +0 -1
  66. package/gui/dist/assets/index-ZmFopEYw.js +0 -52
@@ -281,11 +281,29 @@ export function subagentRosterText(models: Array<{ model: string; efforts: strin
281
281
 
282
282
 
283
283
 
284
+ function isRecord(value: unknown): value is Record<string, unknown> {
285
+ return !!value && typeof value === "object" && !Array.isArray(value);
286
+ }
287
+
288
+ function isGeneratedDeveloperItem(item: unknown, text: string): boolean {
289
+ if (!isRecord(item) || item.type !== "message" || item.role !== "developer") return false;
290
+ if (!Array.isArray(item.content) || item.content.length !== 1) return false;
291
+ const [part] = item.content;
292
+ return isRecord(part) && part.type === "input_text" && part.text === text;
293
+ }
294
+
284
295
  export function injectDeveloperMessage(parsed: OcxParsedRequest, text: string): void {
285
- parsed.context.messages.push({ role: "developer", content: text, timestamp: Date.now() });
286
296
  const raw = parsed._rawBody as { input?: unknown } | undefined;
297
+ const devItem = { type: "message", role: "developer", content: [{ type: "input_text", text }] };
298
+ if (raw && Array.isArray(raw.input)) {
299
+ const replayPrefixLen = Math.min(parsed._replayPrefixLen ?? 0, raw.input.length);
300
+ if (raw.input.slice(0, replayPrefixLen).some(item => isGeneratedDeveloperItem(item, text))) {
301
+ return;
302
+ }
303
+ }
304
+
305
+ parsed.context.messages.push({ role: "developer", content: text, timestamp: Date.now() });
287
306
  if (raw && Array.isArray(raw.input)) {
288
- const devItem = { type: "message", role: "developer", content: [{ type: "input_text", text }] };
289
307
  // compaction_trigger must remain the final input item (codex-rs + ChatGPT backend both
290
308
  // validate this). Insert the developer message BEFORE the trigger when present.
291
309
  const last = raw.input[raw.input.length - 1];
@@ -297,4 +315,3 @@ export function injectDeveloperMessage(parsed: OcxParsedRequest, text: string):
297
315
  }
298
316
  }
299
317
 
300
-
@@ -49,6 +49,7 @@ import {
49
49
  headersForCodexAuthContext,
50
50
  isCodexAuthContextUsable,
51
51
  resolveCodexAuthContext,
52
+ stripCodexRuntimeProviderFields,
52
53
  type CodexAuthContext,
53
54
  } from "../../codex/auth-context";
54
55
  import {
@@ -89,11 +90,15 @@ import type { AttemptRecoveryKind } from "../../usage/log";
89
90
  import {
90
91
  consumeForInspection,
91
92
  consumeForResponseLogMetadata,
93
+ createSseInspector,
92
94
  markNativePassthroughSseResponse,
93
95
  relaySseWithFailedTail,
94
96
  relayWithAbort,
95
97
  sanitizePassthroughHeaders,
96
98
  } from "../relay";
99
+ import { relaySseEagerBounded } from "../relay-eager";
100
+ import { decideEagerRelay } from "../../lib/bun-stream-caps";
101
+ import { cancelBodyOnAbort } from "../../lib/abort";
97
102
  import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
98
103
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
99
104
 
@@ -147,6 +152,45 @@ export function usesCodexForwardPoolAuth(
147
152
  && provider.authMode === "forward" && provider.adapter === "openai-responses";
148
153
  }
149
154
 
155
+ function normalizeCodexUnsupportedModelDetail(value: string): string {
156
+ return value.trim().replace(/\s+/gu, " ").toLocaleLowerCase("en-US");
157
+ }
158
+
159
+ function isAllowListedCodexAccountModel400(
160
+ status: number,
161
+ bodyText: string,
162
+ modelId: string,
163
+ ): boolean {
164
+ if (status !== 400) return false;
165
+ try {
166
+ const payload = JSON.parse(bodyText) as unknown;
167
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
168
+ const detail = (payload as { detail?: unknown }).detail;
169
+ if (typeof detail !== "string") return false;
170
+ const expected = `The '${modelId}' model is not supported when using Codex with a ChatGPT account.`;
171
+ return normalizeCodexUnsupportedModelDetail(detail)
172
+ === normalizeCodexUnsupportedModelDetail(expected);
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ async function shouldRetryCodexPoolAccountModel400(
179
+ response: Response,
180
+ modelId: string,
181
+ signal?: AbortSignal,
182
+ ): Promise<boolean> {
183
+ if (response.status !== 400) return false;
184
+ try {
185
+ const body = await readBoundedResponseBody(response.clone(), { signal });
186
+ return body.displaySafe
187
+ && !body.truncated
188
+ && isAllowListedCodexAccountModel400(response.status, body.text, modelId);
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+
150
194
 
151
195
 
152
196
  export function codexForwardTerminalOutcomeRecorder(
@@ -342,6 +386,22 @@ export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
342
386
  return childHeaders;
343
387
  }
344
388
 
389
+ const UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE =
390
+ "Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.";
391
+
392
+ function unreadableEncryptedAgentTaskResponse(): Response {
393
+ return new Response(
394
+ JSON.stringify({
395
+ error: {
396
+ message: UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE,
397
+ type: "invalid_request_error",
398
+ code: "unreadable_encrypted_agent_task",
399
+ },
400
+ }),
401
+ { status: 400, headers: { "Content-Type": "application/json" } },
402
+ );
403
+ }
404
+
345
405
 
346
406
 
347
407
  export async function handleComboResponses(
@@ -366,9 +426,30 @@ export async function handleComboResponses(
366
426
  return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
367
427
  }
368
428
 
429
+ const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
430
+ (rawBody as { input?: unknown } | undefined)?.input,
431
+ );
432
+ const canDecryptUnreadableAgentTask = (target: (typeof combo.targets)[number]): boolean => {
433
+ const provider = config.providers[target.provider];
434
+ if (!provider || provider.disabled === true) return false;
435
+ try {
436
+ const route = routeModel(config, `${target.provider}/${target.model}`);
437
+ return isCanonicalOpenAiForwardProvider(route.provider);
438
+ } catch {
439
+ return false;
440
+ }
441
+ };
442
+ const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
443
+ !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
444
+
445
+ if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
446
+ return unreadableEncryptedAgentTaskResponse();
447
+ }
448
+
369
449
  const initialNow = Date.now();
370
450
  let pick = pickComboTarget(config, comboId, {
371
- eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
451
+ eligible: target => payloadEligible(target)
452
+ && !isComboTargetInCooldown(comboId, target, initialNow),
372
453
  });
373
454
  if (!pick) {
374
455
  return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
@@ -529,6 +610,7 @@ export async function handleComboResponses(
529
610
  pick = advanceComboAfterFailure(config, pick, {
530
611
  retryAfter: failure.retryAfter,
531
612
  now: Date.now(),
613
+ eligible: payloadEligible,
532
614
  });
533
615
  }
534
616
  return lastFailure!;
@@ -552,12 +634,12 @@ export async function handleResponses(
552
634
  if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
553
635
  return handleComboResponses(req, body, comboId, config, logCtx, options);
554
636
  }
555
- const originalBody = body;
556
- body = expandPreviousResponseInput(body);
557
- const previousResponseInputExpanded = body !== originalBody;
558
637
  const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
559
638
  (body as { input?: unknown } | undefined)?.input,
560
639
  );
640
+ const originalBody = body;
641
+ body = expandPreviousResponseInput(body);
642
+ const previousResponseInputExpanded = body !== originalBody;
561
643
 
562
644
  // Spawn-message compatibility (both directions): agent_message task payloads ride in
563
645
  // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
@@ -580,6 +662,8 @@ export async function handleResponses(
580
662
  if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
581
663
  parsed._providerContinuation = previousResponseProviderState(parsed.previousResponseId);
582
664
  parsed._cursorConversationId = parsed._providerContinuation?.cursor?.conversationId;
665
+ const clientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim();
666
+ if (clientThreadId) parsed._clientThreadId = clientThreadId;
583
667
  } catch (err) {
584
668
  return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
585
669
  }
@@ -605,7 +689,10 @@ export async function handleResponses(
605
689
  (parsed._rawBody as Record<string, unknown>).reasoning = { effort: "low" };
606
690
  }
607
691
  (logCtx as unknown as Record<string, unknown>).shadowCallRewrittenFrom = _sciOriginal;
692
+ // Helpers must not resume/append into the parent thread's Cursor conversation.
693
+ parsed._cursorIsolateConversation = true;
608
694
  }
695
+ if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
609
696
 
610
697
  let route;
611
698
  try {
@@ -621,11 +708,7 @@ export async function handleResponses(
621
708
  // providers cannot. Reject the raw-input classification before adapter construction
622
709
  // or provider dispatch so an unreadable worker task cannot trigger a cost storm.
623
710
  if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
624
- return formatErrorResponse(
625
- 400,
626
- "invalid_request_error",
627
- "Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.",
628
- );
711
+ return unreadableEncryptedAgentTaskResponse();
629
712
  }
630
713
 
631
714
  // Apply the routed model id upstream: routing may strip a "<provider>/" namespace
@@ -772,6 +855,10 @@ export async function handleResponses(
772
855
  }
773
856
  route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
774
857
  logCtx.provider = formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
858
+ // Prefer Codex pool account as the Cursor thread namespace when present. Cursor routes without
859
+ // codexAccountMode still get a credential-derived scope inside the Cursor adapter.
860
+ const identityScope = codexLogAccountId(authCtx);
861
+ if (identityScope) parsed._cursorIdentityScope = identityScope;
775
862
 
776
863
  // OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
777
864
  // existing openai-chat / anthropic adapters authenticate with no change.
@@ -917,7 +1004,7 @@ export async function handleResponses(
917
1004
  + `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
918
1005
  );
919
1006
  }
920
- const request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
1007
+ let request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
921
1008
  const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
922
1009
  ? request.usageLog.inputTokens
923
1010
  : undefined;
@@ -931,6 +1018,20 @@ export async function handleResponses(
931
1018
  linkAbortSignal(upstream, options.abortSignal);
932
1019
  const connectMs = config.connectTimeoutMs ?? 200_000;
933
1020
  let upstreamResponse: Response;
1021
+ const transportFailureResponse = (err: unknown): Response => {
1022
+ upstream.abort();
1023
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
1024
+ const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
1025
+ if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
1026
+ recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
1027
+ threadId: req.headers.get("x-codex-parent-thread-id"),
1028
+ });
1029
+ }
1030
+ const msg = outcome === "timeout"
1031
+ ? `Provider connect timeout after ${connectMs}ms`
1032
+ : `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
1033
+ return formatErrorResponse(502, "upstream_error", msg);
1034
+ };
934
1035
  try {
935
1036
  // Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
936
1037
  // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
@@ -947,18 +1048,80 @@ export async function handleResponses(
947
1048
  { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
948
1049
  );
949
1050
  } catch (err) {
950
- upstream.abort();
951
- if (options.abortSignal?.aborted) return clientCancelledResponse();
952
- const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
953
- if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
954
- recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
1051
+ return transportFailureResponse(err);
1052
+ }
1053
+
1054
+ if (
1055
+ usesCodexForwardPoolAuth(authCtx, route.provider)
1056
+ && await shouldRetryCodexPoolAccountModel400(
1057
+ upstreamResponse,
1058
+ route.modelId,
1059
+ options.abortSignal,
1060
+ )
1061
+ ) {
1062
+ const firstAuthCtx = authCtx;
1063
+ let retryAuthCtx: CodexAuthContext | undefined;
1064
+ try {
1065
+ retryAuthCtx = await resolveCodexAuthContext(
1066
+ req.headers,
1067
+ config,
1068
+ "pool",
1069
+ { excludeAccountId: firstAuthCtx.accountId },
1070
+ );
1071
+ } catch (error) {
1072
+ if (
1073
+ !(error instanceof CodexPoolAuthenticationError)
1074
+ && !(error instanceof CodexAuthContextError)
1075
+ && !(error instanceof CodexAccountCooldownError)
1076
+ ) throw error;
1077
+ }
1078
+
1079
+ if (retryAuthCtx?.kind === "pool" || retryAuthCtx?.kind === "main-pool") {
1080
+ recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, 400, {
955
1081
  threadId: req.headers.get("x-codex-parent-thread-id"),
956
1082
  });
1083
+
1084
+ const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx);
1085
+ const retryProvider = applyCodexAuthContextToProvider(
1086
+ stripCodexRuntimeProviderFields(route.provider),
1087
+ retryAuthCtx,
1088
+ "pool",
1089
+ );
1090
+ const retryAdapter = resolveAdapter(
1091
+ resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider),
1092
+ config.cacheRetention,
1093
+ );
1094
+ request = await retryAdapter.buildRequest(parsed, { headers: retryHeaders });
1095
+
1096
+ await upstreamResponse.body?.cancel().catch(() => undefined);
1097
+ authCtx = retryAuthCtx;
1098
+ options.onCodexAuthContextResolved?.(retryAuthCtx);
1099
+ selectedForwardHeaders = retryHeaders;
1100
+ route.provider = retryProvider;
1101
+ logCtx.provider = formatCodexProviderForLog(
1102
+ route.providerName,
1103
+ retryAuthCtx.accountId,
1104
+ config,
1105
+ );
1106
+
1107
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate);
1108
+ try {
1109
+ upstreamResponse = await fetchWithHeaderTimeout(
1110
+ request.url,
1111
+ {
1112
+ method: request.method,
1113
+ headers: request.headers,
1114
+ body: request.body,
1115
+ },
1116
+ upstream.signal,
1117
+ connectMs,
1118
+ parsed.stream,
1119
+ providerFetch(route.provider),
1120
+ );
1121
+ } catch (err) {
1122
+ return transportFailureResponse(err);
1123
+ }
957
1124
  }
958
- const msg = outcome === "timeout"
959
- ? `Provider connect timeout after ${connectMs}ms`
960
- : `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
961
- return formatErrorResponse(502, "upstream_error", msg);
962
1125
  }
963
1126
  const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
964
1127
  const resolvedModel = headers.get("openai-model")?.trim();
@@ -1021,9 +1184,57 @@ export async function handleResponses(
1021
1184
  // async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
1022
1185
  // native relay, never enters JS Sink.write); branch[1] is consumed in the
1023
1186
  // background for terminal-outcome/quota inspection only.
1187
+ // #314 alternative shape: on win32 (no repair) with a runtime carrying the
1188
+ // Bun#32111 fix — or explicit `streamMode: "eager-relay"` opt-in — the tee
1189
+ // is skipped entirely and relaySseEagerBounded provides a single eager
1190
+ // bounded reader with inline inspection (see src/server/relay-eager.ts and
1191
+ // devlog/_plan/260723_win_mem_safestream/020). Default on the bundled
1192
+ // known-bad runtime remains the tee path below.
1024
1193
  if (upstreamResponse.ok && isEventStream && upstreamResponse.body) {
1025
- const [nativeBody, inspectBody] = upstreamResponse.body.tee();
1026
1194
  const repairConfig = route.provider.responsesItemIdRepair;
1195
+ const winNoRepair = process.platform === "win32" && !hasResponsesItemIdRepair(repairConfig);
1196
+ const eagerDecision = winNoRepair ? decideEagerRelay(config.streamMode ?? "auto") : null;
1197
+ if (eagerDecision?.useEagerRelay) {
1198
+ const turnAc = new AbortController();
1199
+ linkAbortSignal(upstream, turnAc.signal);
1200
+ registerTurn(turnAc);
1201
+ const reportNativeTerminal = recordTerminalOutcomes
1202
+ ? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
1203
+ terminalRecorder?.(status, httpStatusOverride);
1204
+ options.onNativePassthroughTerminal?.(status);
1205
+ }
1206
+ : undefined;
1207
+ const inspector = createSseInspector({
1208
+ onTerminal: reportNativeTerminal,
1209
+ logCtx,
1210
+ onCompletedResponse: rememberPassthroughResponse,
1211
+ onFirstOutput: options.onFirstOutput,
1212
+ });
1213
+ const eagerBody = relaySseEagerBounded(upstreamResponse.body, turnAc, {
1214
+ inspectChunk: chunk => inspector.feed(chunk),
1215
+ finishInspection: () => inspector.finish(),
1216
+ sawTerminal: () => inspector.reported(),
1217
+ onSynthetic: kind => {
1218
+ if (!reportNativeTerminal) return;
1219
+ if (kind === "incomplete") {
1220
+ logCtx.terminalSource = "synthetic";
1221
+ reportNativeTerminal("incomplete");
1222
+ } else {
1223
+ logCtx.transportPhase = "mid_stream";
1224
+ logCtx.terminalSource = "synthetic";
1225
+ reportNativeTerminal("failed", 502);
1226
+ }
1227
+ },
1228
+ onClientCancel: () => options.onNativePassthroughCancel?.(),
1229
+ onDone: () => unregisterTurn(turnAc),
1230
+ });
1231
+ if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
1232
+ return markNativePassthroughSseResponse(new Response(eagerBody, {
1233
+ status: upstreamResponse.status,
1234
+ headers,
1235
+ }));
1236
+ }
1237
+ const [nativeBody, inspectBody] = upstreamResponse.body.tee();
1027
1238
  const turnAc = new AbortController();
1028
1239
  linkAbortSignal(upstream, turnAc.signal);
1029
1240
  registerTurn(turnAc);
@@ -1102,7 +1313,9 @@ export async function handleResponses(
1102
1313
  if (adapter.runTurn) {
1103
1314
  const runTurnAbort = new AbortController();
1104
1315
  linkAbortSignal(runTurnAbort, options.abortSignal);
1105
- const queue = createAdapterEventQueue();
1316
+ const queue = createAdapterEventQueue({
1317
+ onBacklogExceeded: () => runTurnAbort.abort(),
1318
+ });
1106
1319
  const runTurn = async (): Promise<void> => {
1107
1320
  try {
1108
1321
  noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
@@ -1419,6 +1632,8 @@ export async function handleResponses(
1419
1632
  }
1420
1633
  }
1421
1634
 
1635
+ cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
1636
+
1422
1637
  if (parsed.stream) {
1423
1638
  const eventStream = activeAdapter.parseStream(upstreamResponse);
1424
1639
  const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
@@ -104,52 +104,131 @@ export function looksLikeBackendCiphertext(payload: string): boolean {
104
104
 
105
105
 
106
106
 
107
- export const FERNET_TOKEN_RUN = /gAAAA[A-Za-z0-9_-]{60,}={0,2}/g;
107
+ /**
108
+ * Backend-minted ciphertext runs are Fernet tokens (base64url, version byte 0x80).
109
+ * Used to carve embedded blobs out of MIXED slots: plugin hooks may prepend
110
+ * plaintext control metadata to a task body that is already backend-encrypted.
111
+ */
112
+ const FERNET_TOKEN_CANDIDATE = /g[A-Za-z0-9_-]{97,}={0,2}/g;
113
+ const FERNET_TOKEN_BOUNDARY_CHAR = /[A-Za-z0-9_=-]/;
114
+
115
+ interface FernetTokenRun {
116
+ index: number;
117
+ token: string;
118
+ }
119
+
120
+ /**
121
+ * Validate only the key-independent Fernet wire structure. Authenticity cannot be
122
+ * checked without the backend key, but a real token must still be canonical base64url
123
+ * containing version(1) + timestamp(8) + IV(16) + AES-CBC ciphertext(16*n) + HMAC(32).
124
+ * Timestamp freshness is deliberately not enforced: old history can contain valid tokens.
125
+ */
126
+ function isStructurallyValidFernetToken(token: string): boolean {
127
+ if (token.length < 100 || token.length % 4 !== 0) return false;
128
+ if (!/^[A-Za-z0-9_-]+={0,2}$/.test(token)) return false;
129
+
130
+ const unpadded = token.replace(/=+$/, "");
131
+ const paddingLength = token.length - unpadded.length;
132
+ const expectedPadding = (4 - (unpadded.length % 4)) % 4;
133
+ if (expectedPadding > 2 || paddingLength !== expectedPadding) return false;
134
+
135
+ let decoded: Buffer;
136
+ try {
137
+ decoded = Buffer.from(unpadded, "base64url");
138
+ } catch {
139
+ return false;
140
+ }
141
+ if (decoded.toString("base64url") !== unpadded) return false;
142
+ if (decoded.length < 73 || decoded[0] !== 0x80) return false;
143
+
144
+ const ciphertextLength = decoded.length - 57;
145
+ return ciphertextLength >= 16 && ciphertextLength % 16 === 0;
146
+ }
147
+
148
+ /** Maximal, boundary-delimited and structurally valid Fernet runs embedded in a slot. */
149
+ function fernetTokenRuns(payload: string): FernetTokenRun[] {
150
+ const runs: FernetTokenRun[] = [];
151
+ for (const match of payload.matchAll(FERNET_TOKEN_CANDIDATE)) {
152
+ const index = match.index ?? 0;
153
+ const token = match[0];
154
+ const before = index > 0 ? payload[index - 1] : undefined;
155
+ const after = payload[index + token.length];
156
+ if (before && FERNET_TOKEN_BOUNDARY_CHAR.test(before)) continue;
157
+ if (after && FERNET_TOKEN_BOUNDARY_CHAR.test(after)) continue;
158
+ if (!isStructurallyValidFernetToken(token)) continue;
159
+ runs.push({ index, token });
160
+ }
161
+ return runs;
162
+ }
163
+
164
+ function textWithoutFernetRuns(payload: string, runs: readonly FernetTokenRun[]): string {
165
+ let last = 0;
166
+ let text = "";
167
+ for (const run of runs) {
168
+ text += `${payload.slice(last, run.index)}\n\n`;
169
+ last = run.index + run.token.length;
170
+ }
171
+ return `${text}${payload.slice(last)}`;
172
+ }
108
173
 
109
174
  export const AGENT_MESSAGE_ROUTING_ENVELOPE = /(?:^|\n)Message Type\s*:\s*NEW_TASK[^\n]*\nTask name\s*:[^\n]*\nSender\s*:[^\n]*\nPayload\s*:\s*(?:\n|$)/gi;
110
175
 
111
- export const AGENT_MESSAGE_CONTROL_PREAMBLE = /(?:^|\n)\[CXC-(?:LEAF-GUARD|SKILL-AFFORDANCE)\][\s\S]*?(?=\n{2,}|$)/g;
176
+ // CXC is the compatibility-hook control namespace. Strip only the tagged paragraph:
177
+ // later untagged paragraphs may be genuine task text. Repeated CXC paragraphs are
178
+ // removed independently, and a following routing envelope remains available to the
179
+ // envelope stripper below.
180
+ export const AGENT_MESSAGE_CONTROL_PREAMBLE = /(?:^|\n)\[CXC-[A-Z0-9-]+\][^\n]*(?:\n(?!\n|Message Type\s*:)[^\n]*)*(?=\n{2,}|\nMessage Type\s*:|$)/gi;
112
181
 
113
182
  export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
114
183
  if (!Array.isArray(input)) return false;
115
184
 
116
- return input.some(item => {
117
- if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "agent_message") {
118
- return false;
119
- }
185
+ // codex-rs appends one NEW_TASK agent_message at the current input tail. Historical
186
+ // agent messages may be adjacent in full-history bodies; they must not poison the
187
+ // later task. compaction_trigger/additional_tools are trailing metadata rather than
188
+ // a newer user turn.
189
+ let index = input.length - 1;
190
+ while (index >= 0) {
191
+ const item = input[index];
192
+ const type = item && typeof item === "object" ? (item as { type?: unknown }).type : undefined;
193
+ if (type !== "compaction_trigger" && type !== "additional_tools") break;
194
+ index -= 1;
195
+ }
196
+ const item = input[index];
197
+ if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "agent_message") {
198
+ return false;
199
+ }
120
200
 
121
- const content = (item as { content?: unknown }).content;
122
- if (!Array.isArray(content)) return false;
123
-
124
- let hasFernetTask = false;
125
- const readableParts: string[] = [];
126
- for (const part of content) {
127
- if (!part || typeof part !== "object") continue;
128
- const record = part as { type?: unknown; text?: unknown; encrypted_content?: unknown };
129
- if (
130
- (record.type === "input_text" || record.type === "text" || record.type === "output_text")
131
- && typeof record.text === "string"
132
- ) {
133
- readableParts.push(record.text);
134
- continue;
135
- }
136
- if (record.type !== "encrypted_content" || typeof record.encrypted_content !== "string") {
137
- continue;
138
- }
201
+ const content = (item as { content?: unknown }).content;
202
+ if (!Array.isArray(content)) return false;
139
203
 
140
- const withoutFernet = record.encrypted_content.replace(FERNET_TOKEN_RUN, "\n\n");
141
- if (withoutFernet !== record.encrypted_content) hasFernetTask = true;
142
- readableParts.push(withoutFernet);
204
+ let hasFernetTask = false;
205
+ const readableParts: string[] = [];
206
+ for (const part of content) {
207
+ if (!part || typeof part !== "object") continue;
208
+ const record = part as { type?: unknown; text?: unknown; encrypted_content?: unknown };
209
+ if (
210
+ (record.type === "input_text" || record.type === "text")
211
+ && typeof record.text === "string"
212
+ ) {
213
+ readableParts.push(record.text);
214
+ continue;
215
+ }
216
+ if (record.type !== "encrypted_content" || typeof record.encrypted_content !== "string") {
217
+ continue;
143
218
  }
144
219
 
145
- if (!hasFernetTask) return false;
146
- const readableTask = readableParts
147
- .join("\n\n")
148
- .replace(AGENT_MESSAGE_ROUTING_ENVELOPE, "\n")
149
- .replace(AGENT_MESSAGE_CONTROL_PREAMBLE, "\n")
150
- .trim();
151
- return readableTask.length === 0;
152
- });
220
+ const runs = fernetTokenRuns(record.encrypted_content);
221
+ if (runs.length > 0) hasFernetTask = true;
222
+ readableParts.push(textWithoutFernetRuns(record.encrypted_content, runs));
223
+ }
224
+
225
+ if (!hasFernetTask) return false;
226
+ const readableTask = readableParts
227
+ .join("\n\n")
228
+ .replace(AGENT_MESSAGE_CONTROL_PREAMBLE, "\n")
229
+ .replace(AGENT_MESSAGE_ROUTING_ENVELOPE, "\n")
230
+ .trim();
231
+ return readableTask.length === 0;
153
232
  }
154
233
 
155
234
 
@@ -157,12 +236,11 @@ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
157
236
  export function encryptedSlotParts(payload: string): Array<Record<string, string>> {
158
237
  const parts: Array<Record<string, string>> = [];
159
238
  let last = 0;
160
- for (const match of payload.matchAll(FERNET_TOKEN_RUN)) {
161
- const index = match.index ?? 0;
162
- const before = payload.slice(last, index);
239
+ for (const run of fernetTokenRuns(payload)) {
240
+ const before = payload.slice(last, run.index);
163
241
  if (before.trim().length > 0) parts.push({ type: "input_text", text: before });
164
- parts.push({ type: "encrypted_content", encrypted_content: match[0] });
165
- last = index + match[0].length;
242
+ parts.push({ type: "encrypted_content", encrypted_content: run.token });
243
+ last = run.index + run.token.length;
166
244
  }
167
245
  const rest = payload.slice(last);
168
246
  if (rest.trim().length > 0) parts.push({ type: "input_text", text: rest });
@@ -228,4 +306,3 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
228
306
  return rewritten;
229
307
  }
230
308
 
231
-
@@ -22,6 +22,13 @@ export interface WsData {
22
22
  authContext?: CodexAuthContext; // last resolved account decision for observability/registry cleanup
23
23
  cancel?: () => void; // cancels the in-flight stream reader/fetch
24
24
  turnId?: number; // monotonically increasing per socket; prevents stale frames after replacement turns
25
+ /** Discriminator: Responses reframing vs transparent live/realtime sideband relay. */
26
+ kind?: "responses" | "live-sideband";
27
+ liveUpstream?: WebSocket;
28
+ liveUpstreamUrl?: string;
29
+ liveUpstreamHeaders?: Record<string, string>;
30
+ livePending?: Array<string | Buffer>;
31
+ liveOpened?: boolean;
25
32
  }
26
33
 
27
34
  export class WsSendDroppedError extends Error {
package/src/types.ts CHANGED
@@ -5,10 +5,24 @@ export interface OcxParsedRequest {
5
5
  stream: boolean;
6
6
  options: OcxRequestOptions;
7
7
  _rawBody?: unknown;
8
+ /** Number of leading raw input items restored from local previous_response_id state. */
9
+ _replayPrefixLen?: number;
8
10
  /** True when the proxy expanded a previous_response_id request into a full input replay. */
9
11
  _previousResponseInputExpanded?: boolean;
10
12
  /** Provider-private stable Cursor conversation id resolved from the Responses previous_response_id chain. */
11
13
  _cursorConversationId?: string;
14
+ /** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
15
+ _clientThreadId?: string;
16
+ /**
17
+ * Optional authenticated tenant/operator namespace for Cursor thread→conversation derivation.
18
+ * When absent (single-operator local proxy), derivation stays local-scoped.
19
+ */
20
+ _cursorIdentityScope?: string;
21
+ /**
22
+ * True for helper/shadow/compaction turns that must not append into the main Cursor conversation
23
+ * derived from the parent thread id.
24
+ */
25
+ _cursorIsolateConversation?: boolean;
12
26
  /** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
13
27
  _providerContinuation?: OcxProviderContinuationState;
14
28
  /**
@@ -445,6 +459,15 @@ export interface OcxConfig {
445
459
  * Undefined = passthrough (don't modify what the client sends).
446
460
  */
447
461
  fastMode?: boolean;
462
+ /**
463
+ * Windows SSE passthrough stream shape (#314 mitigation).
464
+ * "auto" (default): eager bounded relay only on runtimes proven to carry the
465
+ * Bun#32111 fix (none today → legacy tee). "eager-relay": force the new relay
466
+ * (accepts #32111 crash risk on Bun 1.3.14). "legacy-tee": pin the tee path.
467
+ * Persisted in config.json because Windows services do not inherit shell env.
468
+ * See src/lib/bun-stream-caps.ts.
469
+ */
470
+ streamMode?: "auto" | "legacy-tee" | "eager-relay";
448
471
  /**
449
472
  * Custom override for the injected multi-agent guidance body (the text inside the
450
473
  * <multi_agent_mode> tags). When set, it replaces the built-in prompt on whichever
@@ -529,6 +552,8 @@ export interface OcxConfig {
529
552
  apiKeys?: Array<{ id: string; name: string; key: string; createdAt: string }>;
530
553
  /** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
531
554
  codexAutoStart?: boolean;
555
+ /** Restore an installed shim after a stable external Codex update replaces it. Default true. */
556
+ codexShimAutoRestore?: boolean;
532
557
  /**
533
558
  * Compatibility mode: temporarily rewrite Codex resume-history metadata while the proxy is active
534
559
  * so Codex App can show old OpenAI chats and opencodex-created exec chats under its default
package/src/usage/cost.ts CHANGED
Binary file