@bitkyc08/opencodex 2.81.0 → 2.82.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 (212) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  11. package/src/adapters/openai-responses/passthrough.ts +9 -4
  12. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  13. package/src/claude/agents-inject.ts +14 -3
  14. package/src/claude/context-windows.ts +19 -17
  15. package/src/claude/desktop-3p.ts +88 -12
  16. package/src/claude/desktop-profile.ts +2 -1
  17. package/src/claude/gateway-cache.ts +18 -1
  18. package/src/claude/inbound.ts +77 -3
  19. package/src/claude/intercept/cli-catalog.ts +3 -0
  20. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  21. package/src/claude/intercept/picker-models.ts +14 -6
  22. package/src/claude/intercept/settings.ts +3 -0
  23. package/src/claude/long-context.ts +66 -0
  24. package/src/claude/model-info.ts +19 -16
  25. package/src/claude/outbound.ts +94 -9
  26. package/src/claude/subagent-model.ts +40 -11
  27. package/src/cli/agent.ts +9 -5
  28. package/src/cli/aside-profile-recovery.ts +33 -0
  29. package/src/cli/capabilities-base.ts +5 -5
  30. package/src/cli/capabilities-integrations.ts +2 -2
  31. package/src/cli/cli-path-diagnostics.ts +132 -0
  32. package/src/cli/desktop-runtime-guidance.ts +65 -0
  33. package/src/cli/dispatch.ts +5 -0
  34. package/src/cli/doctor.ts +16 -3
  35. package/src/cli/index.ts +14 -9
  36. package/src/cli/integration-aside-sync.ts +2 -1
  37. package/src/cli/integrations.ts +16 -3
  38. package/src/cli/resolve.ts +25 -2
  39. package/src/cli/root.ts +4 -2
  40. package/src/cli/status.ts +64 -12
  41. package/src/cli/update-restart-child.ts +4 -2
  42. package/src/cli/update-restart-home.ts +28 -10
  43. package/src/cli/update-restart-service-record.ts +116 -0
  44. package/src/cli/update-restart-supervision.ts +96 -0
  45. package/src/cli/update-restart.ts +59 -13
  46. package/src/cli/v2.ts +27 -20
  47. package/src/cli/version-skew-notice.ts +77 -0
  48. package/src/clients/lazycodex.ts +1 -1
  49. package/src/clients/omo-role-models.ts +32 -7
  50. package/src/codex/auth-api/account-selection.ts +107 -0
  51. package/src/codex/auth-api/routes.ts +10 -49
  52. package/src/codex/auth-context.ts +20 -1
  53. package/src/codex/autostart-health.ts +12 -3
  54. package/src/codex/catalog/build-entries.ts +18 -0
  55. package/src/codex/catalog/bundled.ts +155 -26
  56. package/src/codex/catalog/combo-member.ts +3 -2
  57. package/src/codex/catalog/effort.ts +26 -5
  58. package/src/codex/catalog/gather-capture.ts +5 -0
  59. package/src/codex/catalog/model-hints.ts +15 -8
  60. package/src/codex/catalog/model-visibility.ts +3 -1
  61. package/src/codex/catalog/operator-tiers.ts +38 -0
  62. package/src/codex/catalog/provider-models.ts +43 -35
  63. package/src/codex/catalog/retained-sync.ts +7 -5
  64. package/src/codex/catalog/routed-gather.ts +15 -1
  65. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  66. package/src/codex/catalog-auto-refresh.ts +19 -0
  67. package/src/codex/catalog.ts +1 -1
  68. package/src/codex/codex-home-owner.ts +3 -0
  69. package/src/codex/config-write-lock.ts +333 -0
  70. package/src/codex/convergence.ts +2 -1
  71. package/src/codex/features.ts +257 -200
  72. package/src/codex/inject/config-write-section.ts +34 -0
  73. package/src/codex/inject/multi-agent-v2.ts +14 -6
  74. package/src/codex/inject/remove.ts +36 -11
  75. package/src/codex/inject/restore.ts +85 -20
  76. package/src/codex/inject-coordination.ts +15 -6
  77. package/src/codex/inject.ts +62 -20
  78. package/src/codex/journal.ts +62 -17
  79. package/src/codex/main-account-cache.ts +43 -0
  80. package/src/codex/paths.ts +2 -0
  81. package/src/codex/prepared-home.ts +18 -0
  82. package/src/codex/prompt-journal.ts +25 -12
  83. package/src/codex/prompt-layers.ts +49 -26
  84. package/src/codex/prompt-lock-claim.ts +134 -0
  85. package/src/codex/prompt-lock-evidence.ts +26 -0
  86. package/src/codex/prompt-lock-io.ts +16 -0
  87. package/src/codex/prompt-lock-owner.ts +130 -0
  88. package/src/codex/prompt-lock.ts +84 -53
  89. package/src/codex/routing/failure-window.ts +146 -0
  90. package/src/codex/routing/selection.ts +4 -1
  91. package/src/codex/routing.ts +13 -17
  92. package/src/codex/runtime.ts +106 -18
  93. package/src/codex/shim.ts +45 -19
  94. package/src/combos/jev-service-exchange.ts +281 -0
  95. package/src/combos/jev.ts +34 -220
  96. package/src/config/atomic-write.ts +1 -1
  97. package/src/config/codex-ws-reuse-setting.ts +10 -0
  98. package/src/config/live-reconcile.ts +17 -0
  99. package/src/config/load-degrade.ts +8 -0
  100. package/src/config/rebase-provenance.ts +11 -0
  101. package/src/config/schema/config-schema.ts +4 -0
  102. package/src/generated/compatibility-version.json +303 -171
  103. package/src/generated/model-metadata.ts +2 -2
  104. package/src/grok/inject.ts +50 -4
  105. package/src/grok/sync.ts +2 -1
  106. package/src/images/loop.ts +29 -11
  107. package/src/integrations/aside-profiles.ts +10 -3
  108. package/src/integrations/catalog-refresh.ts +18 -2
  109. package/src/integrations/writer.ts +9 -3
  110. package/src/lib/bun-path-runtime.d.mts +32 -0
  111. package/src/lib/bun-path-runtime.mjs +95 -0
  112. package/src/lib/bun-runtime-preflight.ts +140 -0
  113. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  114. package/src/lib/desktop-cli-handoff.mjs +92 -0
  115. package/src/lib/desktop-cli-record.d.mts +29 -0
  116. package/src/lib/desktop-cli-record.mjs +102 -0
  117. package/src/lib/request-execution-budget.ts +8 -0
  118. package/src/lib/synced-state-location.ts +46 -6
  119. package/src/lib/test-home-guard.ts +87 -1
  120. package/src/lib/translator-budget.ts +17 -4
  121. package/src/lib/windows-atomic-replace.ts +38 -16
  122. package/src/lib/windows-elevation.ts +6 -0
  123. package/src/lib/windows-secret-acl.ts +92 -25
  124. package/src/lib/winsw.ts +2 -3
  125. package/src/oauth/anthropic-account-refusal.ts +39 -1
  126. package/src/oauth/anthropic-routing.ts +1 -1
  127. package/src/oauth/anthropic-send-ownership.ts +3 -3
  128. package/src/oauth/chatgpt.ts +113 -41
  129. package/src/oauth/index.ts +6 -2
  130. package/src/oauth/store.ts +1 -1
  131. package/src/providers/azure-model-metadata.ts +115 -0
  132. package/src/providers/derive.ts +21 -0
  133. package/src/providers/quota/vendor-probes-key.ts +127 -17
  134. package/src/providers/quota.ts +1 -1
  135. package/src/responses/reasoning-envelope.ts +29 -1
  136. package/src/responses/reasoning-replay-cache.ts +17 -1
  137. package/src/responses/spill-store.ts +15 -2
  138. package/src/responses/state/metrics.ts +8 -0
  139. package/src/responses/state/spill-failure.ts +12 -0
  140. package/src/responses/state/spill-queue.ts +12 -5
  141. package/src/responses/state.ts +18 -10
  142. package/src/responses/task-input.ts +17 -0
  143. package/src/server/background-lifecycle.ts +27 -0
  144. package/src/server/chat-completions.ts +1 -0
  145. package/src/server/chat-native.ts +28 -12
  146. package/src/server/claude-messages.ts +43 -6
  147. package/src/server/inbound-body-admission.ts +1 -0
  148. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  149. package/src/server/index/serve-options.ts +10 -2
  150. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  151. package/src/server/index/startup-warnings.ts +19 -0
  152. package/src/server/index.ts +1 -0
  153. package/src/server/management/agent-settings-routes.ts +93 -32
  154. package/src/server/management/codex-agent-role-routes.ts +4 -2
  155. package/src/server/management/codex-prompt-routes.ts +1 -0
  156. package/src/server/management/codex-role-auto-assign.ts +13 -2
  157. package/src/server/management/config-routes.ts +59 -8
  158. package/src/server/management/context.ts +2 -2
  159. package/src/server/management/shared.ts +3 -3
  160. package/src/server/messages-native-oauth.ts +9 -8
  161. package/src/server/messages-native.ts +49 -13
  162. package/src/server/messages-response-headers.ts +14 -0
  163. package/src/server/request-log-conversation.ts +48 -5
  164. package/src/server/request-log.ts +31 -2
  165. package/src/server/responses/adapter-continuation.ts +2 -2
  166. package/src/server/responses/adapter-dispatch.ts +34 -13
  167. package/src/server/responses/codex-ws-exchange.ts +17 -2
  168. package/src/server/responses/codex-ws-pool.ts +100 -13
  169. package/src/server/responses/codex-ws-wire.ts +9 -0
  170. package/src/server/responses/combo-requested-effort.ts +3 -1
  171. package/src/server/responses/core-codex-account.ts +22 -1
  172. package/src/server/responses/core-combo-native.ts +20 -3
  173. package/src/server/responses/core-combo.ts +22 -14
  174. package/src/server/responses/core-lifetime.ts +3 -36
  175. package/src/server/responses/core-options.ts +5 -0
  176. package/src/server/responses/core-replay.ts +73 -0
  177. package/src/server/responses/core.ts +3 -3
  178. package/src/server/responses/fetch-helpers.ts +26 -3
  179. package/src/server/responses/passthrough-delivery.ts +24 -2
  180. package/src/server/responses/passthrough-dispatch.ts +19 -2
  181. package/src/server/responses/request-prepare.ts +4 -0
  182. package/src/server/responses/request-send-budget.ts +112 -49
  183. package/src/server/responses/response-effects.ts +1 -0
  184. package/src/server/responses/run-turn-execution.ts +2 -0
  185. package/src/server/responses/sidecar-execution.ts +25 -16
  186. package/src/server/responses/sidecar-send-budget.ts +58 -0
  187. package/src/server/responses/ws-upstream.ts +19 -6
  188. package/src/server/startup-health-cache.ts +2 -1
  189. package/src/server/workflow-refusal.ts +3 -0
  190. package/src/service/cli.ts +20 -4
  191. package/src/service/desktop-command-guard.ts +32 -0
  192. package/src/service/desktop-startup.ts +66 -40
  193. package/src/service/desktop-supervision.d.mts +26 -0
  194. package/src/service/desktop-supervision.mjs +140 -0
  195. package/src/service/managing-cli.ts +1 -0
  196. package/src/service/orchestration.ts +46 -20
  197. package/src/service/repair.ts +25 -9
  198. package/src/service/state.ts +4 -3
  199. package/src/service/systemd.ts +17 -8
  200. package/src/service/windows-ops.ts +61 -22
  201. package/src/storage/policy-job.ts +5 -1
  202. package/src/storage/policy-worker.ts +3 -1
  203. package/src/storage/policy.ts +9 -0
  204. package/src/types/config.ts +21 -0
  205. package/src/types/request.ts +5 -0
  206. package/src/types/wire.ts +1 -0
  207. package/src/update/index.ts +57 -9
  208. package/src/update/restart-ownership.ts +10 -3
  209. package/src/update/runtime-ownership.d.mts +6 -0
  210. package/src/update/runtime-ownership.mjs +23 -7
  211. package/src/web-search/loop.ts +5 -2
  212. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -1,4 +1,5 @@
1
1
  import { createHmac, randomBytes } from "node:crypto";
2
+ import { codexWsReuseAcrossTurnsEnabled } from "../../config/codex-ws-reuse-setting";
2
3
  import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
3
4
  import { CODEX_RESPONSES_HTTP_URL, CODEX_WS_FRAME_HEADERS } from "./codex-ws-request";
4
5
  import { CODEX_WS_ID_MAX_BYTES } from "./codex-ws-correlation";
@@ -7,11 +8,15 @@ import { CodexWsSession } from "./codex-ws-session";
7
8
  export const CODEX_WS_POOL_MAX_SESSIONS = 32;
8
9
  export const CODEX_WS_POOL_IDLE_MS = 30_000;
9
10
  export const CODEX_WS_POOL_MAX_AGE_MS = 5 * 60_000;
11
+ export const CODEX_WS_POOL_WAIT_MS = 750;
12
+ /** Retained sockets per account+thread while cross-turn reuse is on. */
13
+ export const CODEX_WS_POOL_SCOPE_LIMIT = 2;
14
+ const CODEX_WS_POOL_WAIT_DEPTH = 2;
10
15
  const MUTABLE_HEADERS = new Set<string>(CODEX_WS_FRAME_HEADERS);
11
16
  let processKey: Buffer | undefined;
12
17
  let poolSequence = 0;
13
18
 
14
- export interface CodexWsReuseIdentity { key: string; scope: string }
19
+ export interface CodexWsReuseIdentity { key: string; scope: string; routeTag: string }
15
20
  function value(value: unknown): value is string {
16
21
  return typeof value === "string" && value.trim().length > 0
17
22
  && !/[\u0000-\u001f\u007f]/.test(value) && Buffer.byteLength(value) <= CODEX_WS_ID_MAX_BYTES;
@@ -49,19 +54,23 @@ export function codexWsReuseIdentity(url: string, headers: Record<string, string
49
54
  const immutable = Object.entries(headers).filter(([name]) => !MUTABLE_HEADERS.has(name)).sort(([a], [b]) => a.localeCompare(b));
50
55
  if (immutable.length > 128 || immutable.some(([, field]) => !value(field))
51
56
  || immutable.reduce((bytes, [name, field]) => bytes + Buffer.byteLength(name) + Buffer.byteLength(field), 0) > 32 * 1024) return null;
52
- const scope = digest([url, account, thread, turn]);
57
+ const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
58
+ const scope = digest(reuseAcrossTurns ? [url, account, thread] : [url, account, thread, turn]);
53
59
  const lite = metadata.ws_request_header_x_openai_internal_codex_responses_lite;
54
60
  if (lite !== undefined && lite !== "true" && lite !== "false") return null;
55
61
  // `dialUrl` is the socket destination after plugin rewrites; absent means the canonical one.
56
- return { scope, key: digest([scope, authorization, body.model, body.service_tier ?? null, lite ?? null, immutable, proxy ?? null, dialUrl ?? null]) };
62
+ const routeTag = digest(["route", authorization, proxy ?? null, dialUrl ?? null]);
63
+ return { scope, routeTag, key: digest([scope, authorization, body.model, body.service_tier ?? null, lite ?? null, immutable, proxy ?? null, dialUrl ?? null]) };
57
64
  }
58
65
 
59
66
  interface Entry { identity: CodexWsReuseIdentity; session: CodexWsSession; createdAt: number; idleAt: number; retired: boolean }
60
- interface PoolOptions { now?: () => number; maxSessions?: number; idleMs?: number; maxAgeMs?: number }
67
+ interface Waiter { key: string; resolve: (session: CodexWsSession | null) => void; timer: ReturnType<typeof setTimeout>; onAbort: () => void; signal?: AbortSignal }
68
+ interface PoolOptions { now?: () => number; maxSessions?: number; idleMs?: number; maxAgeMs?: number; waitMs?: number }
61
69
 
62
70
  /** Bounded retained sockets only. Busy/capacity misses keep the existing one-shot path. */
63
71
  export class CodexWsPool {
64
72
  private readonly entries = new Map<string, Entry>();
73
+ private readonly waiters: Waiter[] = [];
65
74
  private timer?: ReturnType<typeof setTimeout>;
66
75
  private detachShutdown?: () => void;
67
76
  private readonly hookKey = `codex-upstream-ws-pool-${++poolSequence}`;
@@ -69,29 +78,74 @@ export class CodexWsPool {
69
78
  private readonly maxSessions: number;
70
79
  private readonly idleMs: number;
71
80
  private readonly maxAgeMs: number;
81
+ private readonly waitMs: number;
72
82
  constructor(options: PoolOptions = {}) {
73
83
  this.now = options.now ?? Date.now;
74
84
  this.maxSessions = options.maxSessions ?? CODEX_WS_POOL_MAX_SESSIONS;
75
85
  this.idleMs = options.idleMs ?? CODEX_WS_POOL_IDLE_MS;
76
86
  this.maxAgeMs = options.maxAgeMs ?? CODEX_WS_POOL_MAX_AGE_MS;
87
+ this.waitMs = options.waitMs ?? CODEX_WS_POOL_WAIT_MS;
77
88
  }
78
89
 
79
90
  acquire(identity: CodexWsReuseIdentity, url: string, headers: Record<string, string>, proxy?: string): CodexWsSession | null {
80
- this.sweep();
81
- for (const entry of this.entries.values()) {
82
- if (entry.identity.scope !== identity.scope || entry.identity.key === identity.key) continue;
83
- entry.retired = true;
84
- if (!entry.session.busy) this.remove(entry);
91
+ return this.claim(identity, url, headers, proxy).session;
92
+ }
93
+
94
+ /**
95
+ * Before any frame is sent, wait for the matching busy socket instead of dialing a throwaway.
96
+ * The queue is FIFO and at most two deep. An abort or the deadline returns null.
97
+ */
98
+ acquireWaiting(
99
+ identity: CodexWsReuseIdentity,
100
+ url: string,
101
+ headers: Record<string, string>,
102
+ proxy: string | undefined,
103
+ signal?: AbortSignal,
104
+ ): Promise<CodexWsSession | null> {
105
+ const claimed = this.claim(identity, url, headers, proxy);
106
+ if (claimed.session || !claimed.busy) return Promise.resolve(claimed.session);
107
+ if (signal?.aborted) return Promise.resolve(null);
108
+ if (this.waiters.filter(waiter => waiter.key === identity.key).length >= CODEX_WS_POOL_WAIT_DEPTH) {
109
+ return Promise.resolve(null);
85
110
  }
111
+ return new Promise(resolve => {
112
+ const waiter: Waiter = {
113
+ key: identity.key,
114
+ resolve,
115
+ signal,
116
+ timer: setTimeout(() => this.finishWaiter(waiter, null), this.waitMs),
117
+ onAbort: () => this.finishWaiter(waiter, null),
118
+ };
119
+ waiter.timer.unref?.();
120
+ signal?.addEventListener("abort", waiter.onAbort, { once: true });
121
+ this.waiters.push(waiter);
122
+ });
123
+ }
124
+
125
+ private claim(
126
+ identity: CodexWsReuseIdentity,
127
+ url: string,
128
+ headers: Record<string, string>,
129
+ proxy?: string,
130
+ ): { session: CodexWsSession | null; busy: boolean } {
131
+ this.sweep();
132
+ this.retireSiblings(identity);
86
133
  const existing = this.entries.get(identity.key);
87
134
  if (existing) {
88
- if (existing.retired || existing.session.busy) return null;
89
- if (existing.session.reserve()) { this.arm(); return existing.session; }
135
+ if (existing.retired) return { session: null, busy: false };
136
+ if (existing.session.busy) return { session: null, busy: true };
137
+ if (existing.session.reserve()) { this.arm(); return { session: existing.session, busy: false }; }
90
138
  this.remove(existing);
139
+ } else if (codexWsReuseAcrossTurnsEnabled() && this.scopeCount(identity.scope) >= CODEX_WS_POOL_SCOPE_LIMIT) {
140
+ const oldest = [...this.entries.values()]
141
+ .filter(entry => entry.identity.scope === identity.scope && !entry.session.busy)
142
+ .sort((left, right) => left.idleAt - right.idleAt)[0];
143
+ if (!oldest) return { session: null, busy: false };
144
+ this.remove(oldest);
91
145
  }
92
146
  if (this.entries.size >= this.maxSessions) {
93
147
  const oldest = [...this.entries.values()].filter(entry => !entry.session.busy).sort((a, b) => a.idleAt - b.idleAt)[0];
94
- if (!oldest) return null;
148
+ if (!oldest) return { session: null, busy: false };
95
149
  this.remove(oldest);
96
150
  }
97
151
  const createdAt = this.now();
@@ -100,13 +154,45 @@ export class CodexWsPool {
100
154
  session.reserve();
101
155
  this.entries.set(identity.key, entry);
102
156
  this.detachShutdown ??= registerOptionalShutdownHook(this.hookKey, () => this.dispose());
103
- return session;
157
+ return { session, busy: false };
158
+ }
159
+
160
+ private retireSiblings(identity: CodexWsReuseIdentity): void {
161
+ const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
162
+ for (const entry of this.entries.values()) {
163
+ if (entry.identity.scope !== identity.scope || entry.identity.key === identity.key) continue;
164
+ const routeChanged = entry.identity.routeTag !== identity.routeTag;
165
+ if (reuseAcrossTurns && !routeChanged) continue;
166
+ entry.retired = true;
167
+ if (!entry.session.busy) this.remove(entry);
168
+ }
169
+ }
170
+
171
+ private scopeCount(scope: string): number {
172
+ let count = 0;
173
+ for (const entry of this.entries.values()) if (entry.identity.scope === scope && !entry.retired) count += 1;
174
+ return count;
175
+ }
176
+
177
+ private finishWaiter(waiter: Waiter, session: CodexWsSession | null): void {
178
+ const index = this.waiters.indexOf(waiter);
179
+ if (index < 0) return;
180
+ this.waiters.splice(index, 1);
181
+ clearTimeout(waiter.timer);
182
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
183
+ waiter.resolve(session);
104
184
  }
105
185
 
106
186
  private changed(entry: Entry): void {
107
187
  if (this.entries.get(entry.identity.key) !== entry) return;
108
188
  if (entry.session.closed) this.entries.delete(entry.identity.key);
109
189
  else if (!entry.session.busy) {
190
+ const waiter = this.waiters.find(candidate => candidate.key === entry.identity.key);
191
+ if (waiter && !entry.retired && entry.session.reserve()) {
192
+ this.finishWaiter(waiter, entry.session);
193
+ this.arm();
194
+ return;
195
+ }
110
196
  entry.idleAt = this.now();
111
197
  if (entry.retired || entry.idleAt - entry.createdAt >= this.maxAgeMs) this.remove(entry);
112
198
  }
@@ -152,6 +238,7 @@ export class CodexWsPool {
152
238
  this.detachShutdown = undefined;
153
239
  const entries = [...this.entries.values()];
154
240
  this.entries.clear();
241
+ for (const waiter of [...this.waiters]) this.finishWaiter(waiter, null);
155
242
  for (const entry of entries) entry.session.dispose(new DOMException("codex websocket pool shutdown", "AbortError"));
156
243
  }
157
244
 
@@ -49,6 +49,7 @@ const WS_CLOSE_MESSAGE_TOO_BIG = 1009;
49
49
 
50
50
  const codexWsUpstreamResponses = new WeakSet<Response>();
51
51
  const quotaObservedResponses = new WeakSet<Response>();
52
+ const codexWsPreludeProjections = new WeakSet<Response>();
52
53
 
53
54
  /** Quota arrived directly at its captured account; do not replay old HTTP prelude headers. */
54
55
  export function isCodexWsQuotaObservedResponse(response: Response): boolean {
@@ -60,6 +61,14 @@ export function isCodexWsUpstreamResponse(response: Response): boolean {
60
61
  return codexWsUpstreamResponses.has(response);
61
62
  }
62
63
 
64
+ /** Pre-response projection; its headers retain the exchange's prelude snapshot. */
65
+ export function markCodexWsPreludeProjection(response: Response): void {
66
+ codexWsPreludeProjections.add(response);
67
+ }
68
+
69
+ export function isCodexWsPreludeProjection(response: Response): boolean {
70
+ return codexWsPreludeProjections.has(response);
71
+ }
63
72
 
64
73
  export function markCodexWsResponse(response: Response, observed: boolean): void {
65
74
  codexWsUpstreamResponses.add(response);
@@ -10,5 +10,7 @@ export function comboRequestedEffortLabel(
10
10
  const forcedTransition = forcedEffort != null && forcedEffort !== originalRequestedEffort
11
11
  ? `->${forcedEffort}`
12
12
  : "";
13
- return `${originalRequestedEffort}${forcedTransition}${laterTransitions}`;
13
+ const segments = `${originalRequestedEffort}${forcedTransition}${laterTransitions}`.split("->");
14
+ // A pin that kept the effort it was given is not a transition; drop the repeated segment.
15
+ return segments.filter((segment, index) => index === 0 || segment !== segments[index - 1]).join("->");
14
16
  }
@@ -6,6 +6,7 @@ import {
6
6
  computeQuotaCooldown,
7
7
  formatCodexProviderForLog,
8
8
  } from "../../codex/routing";
9
+ import { claimMainQuotaDispatchForWs, isMainQuotaDispatchLive, renewMainQuotaDispatchForAttempt, type MainQuotaDispatch } from "../../codex/main-account-cache";
9
10
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
10
11
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
11
12
  import { isCodexAccountGenerationLive } from "../../codex/account-store";
@@ -121,8 +122,28 @@ export function usesCodexForwardPoolAuth(
121
122
  }
122
123
 
123
124
 
125
+ /** Live proof that this plain-main response used the observed main credential. */
126
+ export function liveMainQuotaDispatch(
127
+ authCtx: CodexAuthContext, provider: OcxProviderConfig,
128
+ ): MainQuotaDispatch | undefined {
129
+ if (authCtx.kind !== "main" || !authCtx.mainQuotaDispatch) return undefined;
130
+ if (!isCanonicalOpenAiForwardProvider(provider)
131
+ || provider.authMode !== "forward" || provider.adapter !== "openai-responses") return undefined;
132
+ return isMainQuotaDispatchLive(authCtx.mainQuotaDispatch) ? authCtx.mainQuotaDispatch : undefined;
133
+ }
134
+
124
135
  export function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig, modelId?: string): CodexWsQuotaObserver | undefined {
125
- if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
136
+ if (!isCanonicalOpenAiForwardProvider(provider)) return undefined;
137
+ if (!usesCodexForwardPoolAuth(authCtx, provider)) {
138
+ const captured = liveMainQuotaDispatch(authCtx, provider);
139
+ if (!captured || authCtx.kind !== "main") return undefined;
140
+ const dispatch = authCtx.mainQuotaDispatch = renewMainQuotaDispatchForAttempt(captured);
141
+ return headers => {
142
+ claimMainQuotaDispatchForWs(dispatch);
143
+ if (!isMainQuotaDispatchLive(dispatch)) return;
144
+ applyCapturedCodexQuota(MAIN_CODEX_ACCOUNT_ID, headers, dispatch.configGeneration, dispatch.writer, { modelId });
145
+ };
146
+ }
126
147
  const { accountId, writerGeneration } = authCtx;
127
148
  const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined;
128
149
  const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
@@ -32,7 +32,7 @@ import {
32
32
  type RequestLogContext,
33
33
  } from "../request-log";
34
34
  import { captureRouteStaticPolicy, routeConcreteModel } from "../../router";
35
- import { comboDefaultEffort, concreteComboRequestBody, getCombo } from "../../combos";
35
+ import { comboDefaultEffort, concreteComboRequestBody, getCombo, type JevDecision } from "../../combos";
36
36
  import { supportedLadderFor } from "../effort-policy";
37
37
  import { applyDroidReasoningDefault } from "../droid-reasoning-default";
38
38
  import { resolveWireProtocolOverride } from "../adapter-resolve";
@@ -94,6 +94,7 @@ export interface ComboProtocolLanes {
94
94
  target: ComboTarget,
95
95
  targetRoute: RouteResult,
96
96
  targetSendBudget: TransientSendBudget | undefined,
97
+ initialDecision?: Pick<JevDecision, "effort">,
97
98
  ): NativeComboChildPlan | undefined;
98
99
  }
99
100
 
@@ -101,6 +102,7 @@ interface CandidateVerdict {
101
102
  skip?: ProtocolFeature[];
102
103
  }
103
104
 
105
+ /** Build opt-in native eligibility and child plans; only the initial plan applies a JEV choice. */
104
106
  export function createComboProtocolLanes(input: {
105
107
  source: ComboProtocolSource | undefined;
106
108
  req: Request;
@@ -203,7 +205,7 @@ export function createComboProtocolLanes(input: {
203
205
  400, unrepresentableMessage(features), "invalid_request_error", "unsupported_feature",
204
206
  ), "chat");
205
207
  },
206
- nativeChild: (target, targetRoute, targetSendBudget) => {
208
+ nativeChild: (target, targetRoute, targetSendBudget, initialDecision) => {
207
209
  // Without the request's execution budget a native child could not share its sends, so it
208
210
  // keeps the bridge rather than opening a tracker of its own.
209
211
  if (!isRequestExecutionBudget(targetSendBudget)) return undefined;
@@ -214,7 +216,22 @@ export function createComboProtocolLanes(input: {
214
216
  provider: targetRoute.provider,
215
217
  modelId: targetRoute.modelId,
216
218
  });
217
- applyComboEffort(body, config, comboId, target, targetRoute);
219
+ if (initialDecision) {
220
+ // A validated JEV choice owns only this first dispatch, including explicit no-effort.
221
+ // Do not run ordinary force defaults for null: they require a non-null default.
222
+ delete body.service_tier;
223
+ delete body.thinking_budget;
224
+ delete body.thinking;
225
+ const reasoning = body.reasoning;
226
+ if (reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)) {
227
+ const preserved = { ...reasoning as Rec };
228
+ delete preserved.effort;
229
+ if (Object.keys(preserved).length > 0) body.reasoning = preserved;
230
+ else delete body.reasoning;
231
+ }
232
+ if (initialDecision.effort === null) delete body.reasoning_effort;
233
+ else body.reasoning_effort = initialDecision.effort;
234
+ } else applyComboEffort(body, config, comboId, target, targetRoute);
218
235
  return { route, body, sendBudget: targetSendBudget };
219
236
  },
220
237
  };
@@ -717,30 +717,32 @@ export async function executeComboResponses(
717
717
  const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
718
718
  // The inherited spend tracker observes this parent log, before the child has its own label.
719
719
  logCtx.spendPoolId = targetRoute.providerName;
720
- // The first target seeds the ledger's target identity and charges nothing; every later one
721
- // is a real transition, refused once the declared hops, the alternate-target ledger or the
722
- // request total are spent. `countedExternally` is required: the child charges its own
723
- // physical sends, and charging here as well would halve the cap without saying so.
720
+ // Derive the target's allowance before booking its initial send; the booking occupies it.
721
+ const targetSendBudget = comboSendScope
722
+ ? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
723
+ : options.sendBudget;
724
+ // Every target prepays one send. Only its child may consume that booking; later targets
725
+ // remain bounded by the shared total, transition ledger and per-target holdback.
726
+ // `countedExternally` is required: the child charges its own physical sends against this
727
+ // booking through the exact permit, and charging here as well would halve the cap.
724
728
  const hopDecision = comboSendScope?.reserveDispatch({
725
729
  sendClass: firstComboTarget ? "initial" : "combo-failover",
726
730
  targetKey: `${pick.target.provider}/${pick.target.model}`,
727
731
  countedExternally: true,
728
732
  });
729
- if (hopDecision && hopDecision.allowed) hopDecision.permit.use();
730
- else if (hopDecision && firstComboTarget) {
733
+ if (hopDecision && !hopDecision.allowed && firstComboTarget) {
731
734
  // A refused initial reservation authorizes no child send and has no upstream failure to return.
732
735
  return formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, "request send budget exhausted before combo dispatch");
733
736
  }
734
- else if (hopDecision) {
737
+ else if (hopDecision && !hopDecision.allowed) {
735
738
  // Out of budget is not this target's failure. The established exhaustion contract is to
736
739
  // return the last real upstream answer with its status, headers and any quota body
737
740
  // intact rather than to mint a synthetic error, and a later target only exists because
738
741
  // an earlier one already recorded one.
739
742
  return exhaustedFailure();
740
743
  }
741
- const targetSendBudget = comboSendScope
742
- ? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
743
- : options.sendBudget;
744
+ const initialSend = hopDecision?.allowed ? { permit: hopDecision.permit, producerOwned: false } : undefined;
745
+ try {
744
746
  comboTargetsDispatched += 1;
745
747
  const childLog: RequestLogContext = {
746
748
  model: pick.target.model,
@@ -764,7 +766,7 @@ export async function executeComboResponses(
764
766
  initialJevDecision ? initialJevDecision.effort : comboDefaultEffort(config, comboId),
765
767
  initialJevDecision?.effort === null ? [] : targetReasoningEfforts,
766
768
  combo.reasoningEffortMode,
767
- initialJevDecision !== undefined && initialJevDecision.effort !== null ? "force" : combo.defaultEffortMode,
769
+ initialJevDecision ? (initialJevDecision.effort === null ? "fallback" : "force") : combo.defaultEffortMode,
768
770
  );
769
771
  if (initialJevDecision) {
770
772
  delete childBody.service_tier;
@@ -860,7 +862,7 @@ export async function executeComboResponses(
860
862
  && targetEligible(target)
861
863
  && !isComboTargetInCooldown(comboId, target),
862
864
  );
863
- const nativeChild = protocolLanes?.nativeChild(pick.target, targetRoute, targetSendBudget);
865
+ const nativeChild = protocolLanes?.nativeChild(pick.target, targetRoute, targetSendBudget, initialJevDecision);
864
866
  response = nativeChild ? await dispatchNativeComboChild({
865
867
  source: options.protocolSource!,
866
868
  plan: nativeChild,
@@ -890,6 +892,7 @@ export async function executeComboResponses(
890
892
  // parent arrived with.
891
893
  sendBudget: targetSendBudget,
892
894
  comboAttempt: true,
895
+ comboInitialSend: initialSend,
893
896
  comboDispatchPermit: hopDecision?.allowed ? hopDecision.permit : undefined,
894
897
  comboReplaySnapshot,
895
898
  deferCodexResetDerivedCooldown,
@@ -909,8 +912,8 @@ export async function executeComboResponses(
909
912
  onNativePassthroughCancel: callbackGate.onCancel,
910
913
  onResponseComplete: callbackGate.onResponseComplete,
911
914
  });
912
- // Native Chat rebuilds its own body and does not apply the initial JEV effort.
913
- restoreOriginalRequestedEffort(childLog, nativeChild ? undefined : initialJevDecision?.effort);
915
+ // Both lanes applied the initial choice before provider pins/caps; retain those transitions.
916
+ restoreOriginalRequestedEffort(childLog, initialJevDecision?.effort);
914
917
  } catch (error) {
915
918
  callbackGate.discard();
916
919
  if (options.abortSignal?.aborted) {
@@ -1185,6 +1188,11 @@ export async function executeComboResponses(
1185
1188
  // Waiting or recovery may have observed cancellation after the check above.
1186
1189
  if (options.abortSignal?.aborted) return clientCancelledResponse();
1187
1190
  }
1191
+ } finally {
1192
+ // External receipts make release a no-op after real work. runTurn owns its asynchronous
1193
+ // producer; core transfers cleanup there before returning a streaming response.
1194
+ if (!initialSend?.producerOwned) initialSend?.permit.release();
1195
+ }
1188
1196
  }
1189
1197
  const failure = exhaustedFailure();
1190
1198
  if (
@@ -1,4 +1,5 @@
1
1
  import type { TranslatorBudget } from "../../lib/translator-budget";
2
+ import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
2
3
  import {
3
4
  isNativePassthroughSseResponse,
4
5
  markNativePassthroughSseResponse,
@@ -37,42 +38,8 @@ export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
37
38
 
38
39
 
39
40
 
40
- export function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget): Response {
41
- if (!response.body) {
42
- budget.dispose();
43
- return response;
44
- }
45
- const reader = response.body.getReader();
46
- let finalized = false;
47
- const finalize = () => {
48
- if (finalized) return;
49
- finalized = true;
50
- budget.dispose();
51
- };
52
- const body = new ReadableStream<Uint8Array>({
53
- async pull(controller) {
54
- try {
55
- const result = await reader.read();
56
- if (result.done) {
57
- finalize();
58
- controller.close();
59
- } else {
60
- controller.enqueue(result.value);
61
- }
62
- } catch (error) {
63
- finalize();
64
- controller.error(error);
65
- }
66
- },
67
- async cancel(reason) {
68
- try { await reader.cancel(reason); } finally { finalize(); }
69
- },
70
- });
71
- const finalizedResponse = new Response(body, {
72
- status: response.status,
73
- statusText: response.statusText,
74
- headers: response.headers,
75
- });
41
+ export function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget, signal?: AbortSignal): Response {
42
+ const finalizedResponse = finalizeTranslatorBudgetResponse(response, budget, signal);
76
43
  if (isNativePassthroughSseResponse(response)) {
77
44
  markNativePassthroughSseResponse(finalizedResponse);
78
45
  }
@@ -16,6 +16,7 @@ import type { ExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
16
16
  import type { CallerDirectAuth } from "../../providers/caller-authorization";
17
17
  import type { CompactionRoutingOverride } from "./compaction-routing";
18
18
  import type { TranslatorBudget } from "../../lib/translator-budget";
19
+ import type { NativeReasoningOwner } from "../../responses/reasoning-replay-cache";
19
20
  import type { TransientSendBudget } from "../../lib/upstream-retry";
20
21
  import type { RequestLogContext } from "../request-log";
21
22
  import type { UpstreamHostAdmissionLease } from "../../codex/upstream-host-health";
@@ -134,6 +135,8 @@ export interface HandleResponsesOptions {
134
135
  * it. Omitted means a genuine Responses inbound.
135
136
  */
136
137
  inboundWire?: InboundWire;
138
+ nativeReasoningReplay?: ReadonlyMap<string, string>;
139
+ nativeReasoningMint?: { owner?: NativeReasoningOwner };
137
140
  /** Droid's per-request effort default; each concrete combo or policy target applies it only if its ladder allows it. */
138
141
  droidDefaultEffort?: string;
139
142
  /** PF-07: the Chat source a combo child may send natively; set only by the Chat ingress. */
@@ -158,6 +161,8 @@ export interface HandleResponsesOptions {
158
161
  callerDirectAuth?: CallerDirectAuth | null;
159
162
  /** Internal recursion guard; callers outside this module must not set it. */
160
163
  comboAttempt?: boolean;
164
+ /** Child-owned prepaid initial send; capacity remains charged until dispatch or unsent release. */
165
+ comboInitialSend?: { permit: SingleUseDispatchPermit; producerOwned?: boolean };
161
166
  /** Exact externally booked combo hop, used for its child's spend preflight and send reports. */
162
167
  comboDispatchPermit?: SingleUseDispatchPermit;
163
168
  /** Internal handoff: this combo was selected by shadow-call interception. */
@@ -19,6 +19,9 @@ import {
19
19
  reasoningReplayCodexCredentialIdentity,
20
20
  reasoningReplayKeyCredentialIdentity,
21
21
  durableReplayDestinationIdentity,
22
+ nativeReasoningTag,
23
+ nativeReasoningTagMatches,
24
+ type NativeReasoningOwner,
22
25
  bindReasoningReplayScope,
23
26
  reasoningReplayServingIdentityChanged,
24
27
  reasoningReplayItemStoreChanged,
@@ -118,6 +121,16 @@ export function bindRouteReasoningReplayScope(args: {
118
121
  ...args,
119
122
  modelId: parsed.modelId,
120
123
  });
124
+ const owner = nativeReasoningOwnerForRoute(args);
125
+ if (owner) parsed._nativeReasoningBoundOwner = owner;
126
+ else delete parsed._nativeReasoningBoundOwner;
127
+ if (parsed._nativeReasoningReplay?.size) {
128
+ for (const [blob, tag] of parsed._nativeReasoningReplay) {
129
+ if (!nativeReasoningTagMatches(nativeReasoningTag(owner, blob), tag)) {
130
+ stripNativeReasoningBlob(parsed, blob);
131
+ }
132
+ }
133
+ }
121
134
  const continuationDestinationIdentity = providerContinuationDestinationIdentity(parsed, provider);
122
135
  const continuationOwner = providerContinuationOwnerFromReplayIdentity(
123
136
  replayIdentity && continuationDestinationIdentity
@@ -156,6 +169,66 @@ export function bindRouteReasoningReplayScope(args: {
156
169
  bindProviderContinuationForRoute(parsed, continuationOwner);
157
170
  }
158
171
 
172
+ function stripNativeReasoningBlob(parsed: OcxParsedRequest, blob: string): void {
173
+ const input = (parsed._rawBody as { input?: unknown } | undefined)?.input;
174
+ if (Array.isArray(input)) {
175
+ for (const item of input) {
176
+ if (item && typeof item === "object" && item.type === "reasoning" && item.encrypted_content === blob) {
177
+ delete item.encrypted_content;
178
+ delete item.id;
179
+ }
180
+ }
181
+ }
182
+ for (const message of parsed.context.messages) {
183
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
184
+ for (const part of message.content) {
185
+ if (part.type !== "thinking" || typeof part.signature !== "string") continue;
186
+ try {
187
+ const item: unknown = JSON.parse(part.signature);
188
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
189
+ const reasoning = item as Record<string, unknown>;
190
+ if (reasoning.type !== "reasoning" || reasoning.encrypted_content !== blob) continue;
191
+ delete reasoning.encrypted_content;
192
+ delete reasoning.id;
193
+ part.signature = JSON.stringify(reasoning);
194
+ delete part.itemId;
195
+ } catch { /* A provider signature is not the parser's JSON carrier. */ }
196
+ }
197
+ }
198
+ }
199
+
200
+ export function nativeReasoningOwnerForRoute(args: {
201
+ provider: OcxProviderConfig;
202
+ oauthCredentialSnapshot?: Pick<OAuthAccessSnapshot, "accountId" | "generation">;
203
+ codexAuthContext?: CodexAuthContext;
204
+ }): NativeReasoningOwner | undefined {
205
+ const { provider } = args;
206
+ const salt = thoughtSignatureReplaySalt();
207
+ if (!salt || salt.length < 16) return undefined;
208
+ const destination = durableReplayDestinationIdentity(JSON.stringify([
209
+ provider.baseUrl.trim().replace(/\/+$/, ""), provider.responsesPath ?? "",
210
+ ]));
211
+ let credential: string | undefined;
212
+ if (provider.authMode === "oauth") {
213
+ const snapshot = args.oauthCredentialSnapshot;
214
+ if (snapshot?.accountId && snapshot.generation !== undefined) credential = durableReplayCredentialIdentity(
215
+ "oauth", `${snapshot.accountId}\0${snapshot.generation}`, provider.headers, salt,
216
+ );
217
+ } else if (provider.authMode === "forward") {
218
+ const context = args.codexAuthContext;
219
+ if (context && (context.kind === "pool" || context.kind === "main-pool")
220
+ && context.accountId && context.writerGeneration !== undefined
221
+ && (context.kind !== "pool" || context.generation !== undefined)) {
222
+ credential = durableReplayCredentialIdentity("codex", JSON.stringify([
223
+ context.accountId, context.kind === "pool" ? context.generation : null, context.writerGeneration,
224
+ ]), provider.headers, salt);
225
+ }
226
+ } else if (provider.authMode !== "local") {
227
+ credential = durableReplayCredentialIdentity("key", nonEmptyProviderApiKey(provider), provider.headers, salt);
228
+ }
229
+ return destination && credential ? { destination, credential } : undefined;
230
+ }
231
+
159
232
 
160
233
  function routeReasoningReplayIdentity(args: {
161
234
  providerName: string;
@@ -29,8 +29,6 @@ import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health";
29
29
  import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
30
30
  import { runWithCompactionRecovery } from "./compaction-recovery";
31
31
 
32
- /** Public Responses entry and compatibility exports. Implementations live with their owners. */
33
-
34
32
  /**
35
33
  * Route one `/v1/responses` request through the adapter pipeline: recovery loop, passthrough
36
34
  * wire, image/web-search bridges, and the terminal-guard continuation.
@@ -66,13 +64,15 @@ export async function handleResponses(
66
64
  // Once at ingress, spend observer included: a combo child inherits the parent's holder.
67
65
  sendBudget: options.sendBudget ?? createInferenceSendBudget(req, logCtx),
68
66
  }, handleResponsesInner);
69
- const finalResponse = ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
67
+ const finalResponse = ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget, abortSignal) : response;
70
68
  if (!accountLoad.lease) { release(); return finalResponse; }
71
69
  return finalizeAccountLease(finalResponse, release);
72
70
  } catch (error) {
73
71
  release();
74
72
  if (ownsBudget) translatorBudget.dispose();
75
73
  throw error;
74
+ } finally {
75
+ if (!options.comboInitialSend?.producerOwned) options.comboInitialSend?.permit.release();
76
76
  }
77
77
  }
78
78